diff --git a/MinimedKit.xcodeproj/project.pbxproj b/MinimedKit.xcodeproj/project.pbxproj index 9f518b4..d61622b 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 */; }; @@ -632,7 +631,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 = ""; }; @@ -1084,7 +1082,6 @@ C1E34B7829C7AFF7009A50A5 /* TimeInterval.swift */, C1E34B7A29C7B044009A50A5 /* OSLog.swift */, C1E34B8229C7B1AB009A50A5 /* TimeZone.swift */, - C1E34B8429C7B1D4009A50A5 /* HKUnit.swift */, ); path = Extensions; sourceTree = ""; @@ -1521,7 +1518,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 */, @@ -1830,7 +1826,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; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; LOCALIZED_STRING_MACRO_NAMES = ( NSLocalizedString, @@ -1894,7 +1890,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; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; LOCALIZED_STRING_MACRO_NAMES = ( NSLocalizedString, 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/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()) - }() - -} 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/EnliteSensorDisplayable.swift b/MinimedKit/PumpManager/EnliteSensorDisplayable.swift index c567b11..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 a727836..a34b534 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 @@ -17,7 +18,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 +31,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, @@ -264,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)) + } } } } @@ -428,7 +433,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, @@ -470,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", @@ -478,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 @@ -489,7 +497,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 { @@ -507,13 +515,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) + } } } } @@ -554,7 +566,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 +577,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 { @@ -597,7 +609,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 } @@ -607,7 +621,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 } @@ -617,7 +633,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) + } } } } @@ -739,69 +757,71 @@ 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) - - // Track set change and rewind events for cannula/insulin age - self.updateLastEventDates(from: newPumpEvents) - - // 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 - return NewPumpEvent( - date: event.date, - dose: event.dose?.annotated(with: insulinType), - 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) + + // Track set change and rewind events for cannula/insulin age (DIY) + self.updateLastEventDates(from: newPumpEvents) + + // 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 } - return dose.isReconciledWithHistory && dose.isFinished + 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)") + } + 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)) + } } } } @@ -945,6 +965,13 @@ 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", comment: "Generic title of the minimed pump manager") @@ -1028,32 +1055,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 @@ -1080,6 +1084,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 @@ -1240,7 +1271,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 { @@ -1318,7 +1349,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 }) @@ -1348,7 +1379,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 @@ -1371,7 +1402,7 @@ extension MinimedPumpManager: PumpManager { case .success: let now = self.dateGenerator() - let dose = UnfinalizedDose(tempBasalRate: rate, 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 @@ -1471,13 +1502,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)) @@ -1600,13 +1631,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) } @@ -1621,9 +1652,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/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..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() @@ -73,13 +74,14 @@ 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() } waitForExpectations(timeout: 2) } + @MainActor func testBolusWithUncertainResponseIsReported() { mockMessageSender.responses = [ .readPumpStatus: [mockMessageSender.makeMockResponse(.readPumpStatus, ReadPumpStatusMessageBody(bolusing: false, suspended: false))], @@ -87,7 +89,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() } @@ -101,6 +103,7 @@ class MinimedPumpManagerTests: XCTestCase { XCTAssertEqual(event.dose!.deliveredUnits, 2.3) } + @MainActor func testPendingBolusRemovedIfMissingFromHistory() { mockMessageSender.responses = [ @@ -109,7 +112,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/Mocks/MockPumpManagerDelegate.swift b/MinimedKitTests/Mocks/MockPumpManagerDelegate.swift index 643b963..f698c89 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() @@ -68,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) {} 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, 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 { diff --git a/MinimedKitUI/MinimedPumpManager+UI.swift b/MinimedKitUI/MinimedPumpManager+UI.swift index 2581333..3fae0e3 100644 --- a/MinimedKitUI/MinimedPumpManager+UI.swift +++ b/MinimedKitUI/MinimedPumpManager+UI.swift @@ -88,7 +88,7 @@ extension MinimedPumpManager: PumpManagerUI { // MARK: - PumpStatusIndicator extension MinimedPumpManager { - public var pumpStatusHighlight: DeviceStatusHighlight? { + public var pumpStatusHighlight: PumpStatusHighlight? { return buildPumpStatusHighlight(for: state, recents: recents, andDate: dateGenerator()) } diff --git a/MinimedKitUI/Views/MinimedPumpSettingsViewModel.swift b/MinimedKitUI/Views/MinimedPumpSettingsViewModel.swift index 60ed8ab..be0e5a0 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 { case suspendError(Error) @@ -97,7 +97,7 @@ class MinimedPumpSettingsViewModel: ObservableObject { }() let reservoirVolumeFormatter = { - let formatter = QuantityFormatter(for: .internationalUnit()) + let formatter = QuantityFormatter(for: .internationalUnit) formatter.numberFormatter.maximumFractionDigits = 1 return formatter }() @@ -169,7 +169,7 @@ class MinimedPumpSettingsViewModel: ObservableObject { switch basalDeliveryState { case .active(_), .initiatingTempBasal: return true - case .tempBasal(_), .cancelingTempBasal, .suspending, .suspended(_), .resuming, .none: + default: return false } } @@ -196,8 +196,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) @@ -205,6 +203,8 @@ class MinimedPumpSettingsViewModel: ObservableObject { return pumpManager.state.basalSchedule.currentRate(using: calendar) case .tempBasal(let dose): return dose.unitsPerHour + default: + return nil } } @@ -233,7 +233,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) ?? "" } @@ -298,7 +298,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 @@ -311,7 +311,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")