Skip to content

Prevent Automatic Dosing with Future Glucose #1894

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jan 11, 2023
Merged
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
15 changes: 15 additions & 0 deletions Loop/Managers/LoopDataManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1130,6 +1130,7 @@ extension LoopDataManager {
/// - LoopError.missingDataError
/// - LoopError.configurationError
/// - LoopError.glucoseTooOld
/// - LoopError.invalidFutureGlucose
/// - LoopError.pumpDataTooOld
fileprivate func predictGlucose(
startingAt startingGlucoseOverride: GlucoseValue? = nil,
Expand All @@ -1156,6 +1157,10 @@ extension LoopDataManager {
throw LoopError.glucoseTooOld(date: glucose.startDate)
}

guard lastGlucoseDate.timeIntervalSince(now()) <= LoopCoreConstants.futureGlucoseDataInterval else {
throw LoopError.invalidFutureGlucose(date: lastGlucoseDate)
}

guard now().timeIntervalSince(pumpStatusDate) <= LoopCoreConstants.inputDataRecencyInterval else {
throw LoopError.pumpDataTooOld(date: pumpStatusDate)
}
Expand Down Expand Up @@ -1390,6 +1395,7 @@ extension LoopDataManager {
/// - Throws:
/// - LoopError.missingDataError
/// - LoopError.glucoseTooOld
/// - LoopError.invalidFutureGlucose
/// - LoopError.pumpDataTooOld
/// - LoopError.configurationError
fileprivate func recommendBolusValidatingDataRecency<Sample: GlucoseValue>(forPrediction predictedGlucose: [Sample],
Expand All @@ -1405,6 +1411,10 @@ extension LoopDataManager {
throw LoopError.glucoseTooOld(date: glucose.startDate)
}

guard lastGlucoseDate.timeIntervalSince(now()) <= LoopCoreConstants.inputDataRecencyInterval else {
throw LoopError.invalidFutureGlucose(date: lastGlucoseDate)
}

guard now().timeIntervalSince(pumpStatusDate) <= LoopCoreConstants.inputDataRecencyInterval else {
throw LoopError.pumpDataTooOld(date: pumpStatusDate)
}
Expand Down Expand Up @@ -1516,6 +1526,7 @@ extension LoopDataManager {
/// - Throws:
/// - LoopError.configurationError
/// - LoopError.glucoseTooOld
/// - LoopError.invalidFutureGlucose
/// - LoopError.missingDataError
/// - LoopError.pumpDataTooOld
private func updatePredictedGlucoseAndRecommendedDose(with dosingDecision: StoredDosingDecision) -> (StoredDosingDecision, LoopError?) {
Expand All @@ -1539,6 +1550,10 @@ extension LoopDataManager {
errors.append(.glucoseTooOld(date: glucose.startDate))
}

if glucose.startDate.timeIntervalSince(startDate) > LoopCoreConstants.inputDataRecencyInterval {
errors.append(.invalidFutureGlucose(date: glucose.startDate))
}

let pumpStatusDate = doseStore.lastAddedPumpData

if startDate.timeIntervalSince(pumpStatusDate) > LoopCoreConstants.inputDataRecencyInterval {
Expand Down
10 changes: 10 additions & 0 deletions Loop/Models/LoopError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ enum LoopError: Error {
// Glucose data is too old to perform action
case glucoseTooOld(date: Date)

// Glucose data is in the future
case invalidFutureGlucose(date: Date)

// Pump data is too old to perform action
case pumpDataTooOld(date: Date)

Expand Down Expand Up @@ -120,6 +123,8 @@ extension LoopError {
return "missingDataError"
case .glucoseTooOld:
return "glucoseTooOld"
case .invalidFutureGlucose:
return "invalidFutureGlucose"
case .pumpDataTooOld:
return "pumpDataTooOld"
case .recommendationExpired:
Expand All @@ -142,6 +147,8 @@ extension LoopError {
details["detail"] = detail.rawValue
case .glucoseTooOld(let date):
details["date"] = StoredDosingDecisionIssue.description(for: date)
case .invalidFutureGlucose(let date):
details["date"] = StoredDosingDecisionIssue.description(for: date)
case .pumpDataTooOld(let date):
details["date"] = StoredDosingDecisionIssue.description(for: date)
case .recommendationExpired(let date):
Expand Down Expand Up @@ -183,6 +190,9 @@ extension LoopError: LocalizedError {
case .glucoseTooOld(let date):
let minutes = formatter.string(from: -date.timeIntervalSinceNow) ?? ""
return String(format: NSLocalizedString("Glucose data is %1$@ old", comment: "The error message when glucose data is too old to be used. (1: glucose data age in minutes)"), minutes)
case .invalidFutureGlucose(let date):
let minutes = formatter.string(from: -date.timeIntervalSinceNow) ?? ""
return String(format: NSLocalizedString("Invalid glucose reading with a timestamp that is %1$@ in the future", comment: "The error message when glucose data is in the future. (1: glucose data time in future in minutes)"), minutes)
case .pumpDataTooOld(let date):
let minutes = formatter.string(from: -date.timeIntervalSinceNow) ?? ""
return String(format: NSLocalizedString("Pump data is %1$@ old", comment: "The error message when pump data is too old to be used. (1: pump data age in minutes)"), minutes)
Expand Down
3 changes: 3 additions & 0 deletions Loop/View Models/BolusEntryViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ final class BolusEntryViewModel: ObservableObject {
case predictedGlucoseBelowSuspendThreshold(suspendThreshold: HKQuantity)
case glucoseBelowTarget
case staleGlucoseData
case futureGlucoseData
case stalePumpData
}

Expand Down Expand Up @@ -673,6 +674,8 @@ final class BolusEntryViewModel: ObservableObject {
switch error {
case LoopError.missingDataError(.glucose), LoopError.glucoseTooOld:
notice = .staleGlucoseData
case LoopError.invalidFutureGlucose:
notice = .futureGlucoseData
case LoopError.pumpDataTooOld:
notice = .stalePumpData
default:
Expand Down
5 changes: 5 additions & 0 deletions Loop/Views/BolusEntryView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,11 @@ struct BolusEntryView: View {
title: Text("No Recent Glucose Data", comment: "Title for bolus screen notice when glucose data is missing or stale"),
caption: Text("Enter a blood glucose from a meter for a recommended bolus amount.", comment: "Caption for bolus screen notice when glucose data is missing or stale")
)
case .futureGlucoseData:
return WarningView(
title: Text("Invalid Future Glucose", comment: "Title for bolus screen notice when glucose data is in the future"),
caption: Text("Check your device time and/or remove any invalid data from Apple Health.", comment: "Caption for bolus screen notice when glucose data is in the future")
)
case .stalePumpData:
return WarningView(
title: Text("No Recent Pump Data", comment: "Title for bolus screen notice when pump data is missing or stale"),
Expand Down
3 changes: 3 additions & 0 deletions LoopCore/LoopCoreConstants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ public enum LoopCoreConstants {
/// The amount of time since a given date that input data should be considered valid
public static let inputDataRecencyInterval = TimeInterval(minutes: 15)

/// The amount of time in the future a glucose value should be considered valid
public static let futureGlucoseDataInterval = TimeInterval(minutes: 5)

public static let defaultCarbAbsorptionTimes: CarbStore.DefaultAbsorptionTimes = (fast: .minutes(30), medium: .hours(3), slow: .hours(5))

/// How much historical glucose to include in a dosing decision
Expand Down
20 changes: 20 additions & 0 deletions LoopTests/ViewModels/BolusEntryViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,26 @@ class BolusEntryViewModelTests: XCTestCase {
XCTAssertEqual(.stalePumpData, bolusEntryViewModel.activeNotice)
}

func testUpdateRecommendedBolusThrowsGlucoseTooOld() async throws {
XCTAssertFalse(bolusEntryViewModel.isBolusRecommended)
delegate.loopState.bolusRecommendationError = LoopError.glucoseTooOld(date: now)
await bolusEntryViewModel.update()
XCTAssertFalse(bolusEntryViewModel.isBolusRecommended)
let recommendedBolus = bolusEntryViewModel.recommendedBolus
XCTAssertNil(recommendedBolus)
XCTAssertEqual(.staleGlucoseData, bolusEntryViewModel.activeNotice)
}

func testUpdateRecommendedBolusThrowsInvalidFutureGlucose() async throws {
XCTAssertFalse(bolusEntryViewModel.isBolusRecommended)
delegate.loopState.bolusRecommendationError = LoopError.invalidFutureGlucose(date: now)
await bolusEntryViewModel.update()
XCTAssertFalse(bolusEntryViewModel.isBolusRecommended)
let recommendedBolus = bolusEntryViewModel.recommendedBolus
XCTAssertNil(recommendedBolus)
XCTAssertEqual(.futureGlucoseData, bolusEntryViewModel.activeNotice)
}

func testUpdateRecommendedBolusThrowsOtherError() async throws {
XCTAssertFalse(bolusEntryViewModel.isBolusRecommended)
delegate.loopState.bolusRecommendationError = LoopError.pumpSuspended
Expand Down