Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ extension DeviceDataManager: SimpleBolusViewModelDelegate {
}

func enactBolus(units: Double, activationType: BolusActivationType) {
enactBolus(units: units, activationType: activationType) { (_) in }
// The simple bolus calculator is only ever driven by the user on the phone.
enactBolus(units: units, activationType: activationType, origin: .manual) { (_) in }
}

func computeSimpleBolusRecommendation(at date: Date, mealCarbs: HKQuantity?, manualGlucose: HKQuantity?) -> BolusDosingDecision? {
Expand Down
35 changes: 28 additions & 7 deletions Loop/Managers/DeviceDataManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -829,24 +829,34 @@ extension DeviceDataManager {

// MARK: - Client API
extension DeviceDataManager {
func enactBolus(units: Double, activationType: BolusActivationType, completion: @escaping (_ error: Error?) -> Void = { _ in }) {
func enactBolus(units: Double, activationType: BolusActivationType, origin: BolusOrigin? = nil, completion: @escaping (_ error: Error?) -> Void = { _ in }) {
guard let pumpManager = pumpManager else {
completion(LoopError.configurationError(.pumpManager))
return
}

// Mint the correlation reference only once the command is actually going to the pump, so the guard
// above cannot leave an orphaned origin mapping behind.
let bolusReference = origin.map { BolusOriginStore.shared.makeReference(for: $0) }

self.loopManager.addRequestedBolus(DoseEntry(type: .bolus, startDate: Date(), value: units, unit: .units, isMutable: true)) {
pumpManager.enactBolus(units: units, activationType: activationType) { (error) in
pumpManager.enactBolus(units: units, activationType: activationType, bolusReference: bolusReference) { (error) in
if let error = error {
self.log.error("%{public}@", String(describing: error))
switch error {
case .uncertainDelivery:
// Do not generate notification on uncertain delivery error
// Do not generate notification on uncertain delivery error. Keep the origin mapping:
// the dose may still be reported and reconciled later.
break
default:
// Definite failure: drop the origin mapping for this request. The origin still rides
// along on the failure notification so a retried bolus keeps its provenance.
if let bolusReference = bolusReference {
BolusOriginStore.shared.remove(reference: bolusReference)
}
// Do not generate notifications for automatic boluses that fail.
if !activationType.isAutomatic {
NotificationManager.sendBolusFailureNotification(for: error, units: units, at: Date(), activationType: activationType)
NotificationManager.sendBolusFailureNotification(for: error, units: units, at: Date(), activationType: activationType, origin: origin)
}
}

Expand All @@ -864,9 +874,9 @@ extension DeviceDataManager {
}
}

func enactBolus(units: Double, activationType: BolusActivationType) async throws {
func enactBolus(units: Double, activationType: BolusActivationType, origin: BolusOrigin? = nil) async throws {
return try await withCheckedThrowingContinuation { continuation in
enactBolus(units: units, activationType: activationType) { error in
enactBolus(units: units, activationType: activationType, origin: origin) { error in
if let error = error {
continuation.resume(throwing: error)
return
Expand Down Expand Up @@ -1221,6 +1231,17 @@ extension DeviceDataManager: PumpManagerDelegate {
dispatchPrecondition(condition: .onQueue(queue))
log.default("PumpManager:%{public}@ hasNewPumpEvents (lastReconciliation = %{public}@)", String(describing: type(of: pumpManager)), String(describing: lastReconciliation))

// Re-key any tagged bolus origin from its request reference to the identifier the dose store will
// persist as the dose's syncIdentifier and that is looked up at Nightscout-upload time. The dose store
// always derives that identifier from the event's raw bytes (see PumpEvent.syncIdentifier), so key on
// those directly rather than on dose.syncIdentifier, which is only populated when the event was built
// through NewPumpEvent's designated init.
for event in events {
if let dose = event.dose, dose.type == .bolus, let reference = dose.bolusReference {
BolusOriginStore.shared.promoteReference(reference, toSyncIdentifier: event.raw.hexadecimalString)
}
}

doseStore.addPumpEvents(events, lastReconciliation: lastReconciliation, replacePendingEvents: replacePendingEvents) { (error) in
if let error = error {
self.log.error("Failed to addPumpEvents to DoseStore: %{public}@", String(describing: error))
Expand Down Expand Up @@ -1450,7 +1471,7 @@ extension Notification.Name {
extension DeviceDataManager: ServicesManagerDosingDelegate {

func deliverBolus(amountInUnits: Double) async throws {
try await enactBolus(units: amountInUnits, activationType: .manualNoRecommendation)
try await enactBolus(units: amountInUnits, activationType: .manualNoRecommendation, origin: .remote)
}

}
Expand Down
9 changes: 7 additions & 2 deletions Loop/Managers/LoopAppManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -553,8 +553,13 @@ extension LoopAppManager: UNUserNotificationCenterDelegate {
startDate.timeIntervalSinceNow >= TimeInterval(minutes: -5)
{
deviceDataManager?.analyticsServicesManager.didRetryBolus()

deviceDataManager?.enactBolus(units: units, activationType: activationType) { (_) in

// Restore the failed bolus's origin if the notification carried one; the retry is still
// user-initiated on the phone, so fall back to .manual.
let origin = (response.notification.request.content.userInfo[LoopNotificationUserInfoKey.bolusOrigin.rawValue] as? String)
.flatMap(BolusOrigin.init(rawValue:)) ?? .manual

deviceDataManager?.enactBolus(units: units, activationType: activationType, origin: origin) { (_) in
DispatchQueue.main.async {
completionHandler()
}
Expand Down
7 changes: 5 additions & 2 deletions Loop/Managers/NotificationManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ extension NotificationManager {

// MARK: - Notifications

static func sendBolusFailureNotification(for error: PumpManagerError, units: Double, at startDate: Date, activationType: BolusActivationType) {
static func sendBolusFailureNotification(for error: PumpManagerError, units: Double, at startDate: Date, activationType: BolusActivationType, origin: BolusOrigin? = nil) {
let notification = UNMutableNotificationContent()

notification.title = NSLocalizedString("Bolus Issue", comment: "The notification title for a bolus issue")
Expand All @@ -99,11 +99,14 @@ extension NotificationManager {
notification.categoryIdentifier = LoopNotificationCategory.bolusFailure.rawValue
}

notification.userInfo = [
var userInfo: [String: Any] = [
LoopNotificationUserInfoKey.bolusAmount.rawValue: units,
LoopNotificationUserInfoKey.bolusStartDate.rawValue: startDate,
LoopNotificationUserInfoKey.bolusActivationType.rawValue: activationType.rawValue
]
// Carry the origin so a retry from the notification keeps the bolus's provenance.
userInfo[LoopNotificationUserInfoKey.bolusOrigin.rawValue] = origin?.rawValue
notification.userInfo = userInfo

let request = UNNotificationRequest(
// Only support 1 bolus notification at once
Expand Down
2 changes: 1 addition & 1 deletion Loop/Managers/WatchDataManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ final class WatchDataManager: NSObject {
return
}

deviceManager.enactBolus(units: bolus.value, activationType: bolus.activationType) { (error) in
deviceManager.enactBolus(units: bolus.value, activationType: bolus.activationType, origin: .watch) { (error) in
if error == nil {
self.deviceManager.analyticsServicesManager.didBolus(source: "Watch", units: bolus.value)
}
Expand Down
4 changes: 2 additions & 2 deletions Loop/View Models/BolusEntryViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ protocol BolusEntryViewModelDelegate: AnyObject {

func storeManualBolusDosingDecision(_ bolusDosingDecision: BolusDosingDecision, withDate date: Date)

func enactBolus(units: Double, activationType: BolusActivationType, completion: @escaping (_ error: Error?) -> Void)
func enactBolus(units: Double, activationType: BolusActivationType, origin: BolusOrigin?, completion: @escaping (_ error: Error?) -> Void)

func getGlucoseSamples(start: Date?, end: Date?, completion: @escaping (_ samples: Swift.Result<[StoredGlucoseSample], Error>) -> Void)

Expand Down Expand Up @@ -421,7 +421,7 @@ final class BolusEntryViewModel: ObservableObject {

if amountToDeliver > 0 {
savedPreMealOverride = nil
delegate.enactBolus(units: amountToDeliver, activationType: activationType, completion: { _ in
delegate.enactBolus(units: amountToDeliver, activationType: activationType, origin: .manual, completion: { _ in
self.analyticsServicesManager?.didBolus(source: "Phone", units: amountToDeliver)
})
}
Expand Down
2 changes: 1 addition & 1 deletion LoopTests/ViewModels/BolusEntryViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -911,7 +911,7 @@ fileprivate class MockBolusEntryViewModelDelegate: BolusEntryViewModelDelegate {

var enactedBolusUnits: Double?
var enactedBolusActivationType: BolusActivationType?
func enactBolus(units: Double, activationType: BolusActivationType, completion: @escaping (Error?) -> Void) {
func enactBolus(units: Double, activationType: BolusActivationType, origin: BolusOrigin?, completion: @escaping (Error?) -> Void) {
enactedBolusUnits = units
enactedBolusActivationType = activationType
}
Expand Down