From 42893108b5d8924aab5b8fc40cae430ff64edb01 Mon Sep 17 00:00:00 2001 From: Hannah Kim Date: Wed, 12 Apr 2023 21:22:23 -0400 Subject: [PATCH 01/49] Fixed numeral localization in insulin delivery table view --- Loop/View Controllers/InsulinDeliveryTableViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Loop/View Controllers/InsulinDeliveryTableViewController.swift b/Loop/View Controllers/InsulinDeliveryTableViewController.swift index 7a3f9115b3..c340f8f536 100644 --- a/Loop/View Controllers/InsulinDeliveryTableViewController.swift +++ b/Loop/View Controllers/InsulinDeliveryTableViewController.swift @@ -609,7 +609,7 @@ fileprivate var numberFormatter: NumberFormatter { fileprivate func createAttributedDescription(from description: String, with font: UIFont) -> NSAttributedString? { let descriptionWithFont = String(format:"%@", description) - guard let attributedDescription = try? NSMutableAttributedString(data: Data(descriptionWithFont.utf8), options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil) else { + guard let attributedDescription = try? NSMutableAttributedString(data: descriptionWithFont.data(using: .utf16)!, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil) else { return nil } From d1f6908b69d114b99afce7be4b6a2c06d6fbcff9 Mon Sep 17 00:00:00 2001 From: Anna Quinlan Date: Mon, 1 May 2023 15:14:50 -0700 Subject: [PATCH 02/49] Disable missed meal detection if there are user-entered and/or calibration points --- Loop/Managers/LoopDataManager.swift | 33 +++++++++++++------ .../MealDetectionManager.swift | 24 ++++++++++++-- 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index 18a0816621..aedce6c2aa 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -874,8 +874,8 @@ extension LoopDataManager { logger.default("Loop ended") notify(forChange: .loopFinished) - let carbEffectStart = now().addingTimeInterval(-MissedMealSettings.maxRecency) - carbStore.getGlucoseEffects(start: carbEffectStart, end: now(), effectVelocities: insulinCounteractionEffects) {[weak self] result in + let samplesStart = now().addingTimeInterval(-MissedMealSettings.maxRecency) + carbStore.getGlucoseEffects(start: samplesStart, end: now(), effectVelocities: insulinCounteractionEffects) {[weak self] result in guard let self = self, case .success((_, let carbEffects)) = result @@ -885,15 +885,28 @@ extension LoopDataManager { } return } - - self.mealDetectionManager.generateMissedMealNotificationIfNeeded( - insulinCounteractionEffects: self.insulinCounteractionEffects, - carbEffects: carbEffects, - pendingAutobolusUnits: self.recommendedAutomaticDose?.recommendation.bolusUnits, - bolusDurationEstimator: { [unowned self] bolusAmount in - return self.delegate?.loopDataManager(self, estimateBolusDuration: bolusAmount) + + glucoseStore.getGlucoseSamples(start: samplesStart, end: now()) {[weak self] result in + guard + let self = self, + case .success(let glucoseSamples) = result + else { + if case .failure(let error) = result { + self?.logger.error("Failed to fetch glucose samples to check for missed meal: %{public}@", String(describing: error)) + } + return } - ) + + self.mealDetectionManager.generateMissedMealNotificationIfNeeded( + glucoseSamples: glucoseSamples, + insulinCounteractionEffects: self.insulinCounteractionEffects, + carbEffects: carbEffects, + pendingAutobolusUnits: self.recommendedAutomaticDose?.recommendation.bolusUnits, + bolusDurationEstimator: { [unowned self] bolusAmount in + return self.delegate?.loopDataManager(self, estimateBolusDuration: bolusAmount) + } + ) + } } // 5 second delay to allow stores to cache data before it is read by widget diff --git a/Loop/Managers/Missed Meal Detection/MealDetectionManager.swift b/Loop/Managers/Missed Meal Detection/MealDetectionManager.swift index 5de7972225..a1b8af02ea 100644 --- a/Loop/Managers/Missed Meal Detection/MealDetectionManager.swift +++ b/Loop/Managers/Missed Meal Detection/MealDetectionManager.swift @@ -64,13 +64,22 @@ class MealDetectionManager { } // MARK: Meal Detection - func hasMissedMeal(insulinCounteractionEffects: [GlucoseEffectVelocity], carbEffects: [GlucoseEffect], completion: @escaping (MissedMealStatus) -> Void) { + func hasMissedMeal(glucoseSamples: [some GlucoseSampleValue], insulinCounteractionEffects: [GlucoseEffectVelocity], carbEffects: [GlucoseEffect], completion: @escaping (MissedMealStatus) -> Void) { let delta = TimeInterval(minutes: 5) let intervalStart = currentDate(timeIntervalSinceNow: -MissedMealSettings.maxRecency) let intervalEnd = currentDate(timeIntervalSinceNow: -MissedMealSettings.minRecency) let now = self.currentDate - + + let filteredGlucoseValues = glucoseSamples.filter { intervalStart <= $0.startDate && $0.startDate <= now } + + /// Only try to detect if there's a missed meal if there are no calibration/user-entered BGs, + /// since these can cause large jumps + guard !filteredGlucoseValues.containsUserEntered() else { + completion(.noMissedMeal) + return + } + let filteredCarbEffects = carbEffects.filterDateRange(intervalStart, now) /// Compute how much of the ICE effect we can't explain via our entered carbs @@ -213,12 +222,13 @@ class MealDetectionManager { /// - pendingAutobolusUnits: any autobolus units that are still being delivered. Used to delay the missed meal notification to avoid notifying during an autobolus. /// - bolusDurationEstimator: estimator of bolus duration that takes the units of the bolus as an input. Used to delay the missed meal notification to avoid notifying during an autobolus. func generateMissedMealNotificationIfNeeded( + glucoseSamples: [some GlucoseSampleValue], insulinCounteractionEffects: [GlucoseEffectVelocity], carbEffects: [GlucoseEffect], pendingAutobolusUnits: Double? = nil, bolusDurationEstimator: @escaping (Double) -> TimeInterval? ) { - hasMissedMeal(insulinCounteractionEffects: insulinCounteractionEffects, carbEffects: carbEffects) {[weak self] status in + hasMissedMeal(glucoseSamples: glucoseSamples, insulinCounteractionEffects: insulinCounteractionEffects, carbEffects: carbEffects) {[weak self] status in self?.manageMealNotifications(for: status, pendingAutobolusUnits: pendingAutobolusUnits, bolusDurationEstimator: bolusDurationEstimator) } } @@ -294,3 +304,11 @@ class MealDetectionManager { completionHandler(report.joined(separator: "\n")) } } + +fileprivate extension BidirectionalCollection where Element: GlucoseSampleValue, Index == Int { + /// Returns whether there are any user-entered or calibration points + /// Runtime: O(n) + func containsUserEntered() -> Bool { + return !isCalibrated() || filter({ $0.wasUserEntered }).count != 0 + } +} From 4e244c8eb504fe721d0add46ed17ac4c0827a762 Mon Sep 17 00:00:00 2001 From: Anna Quinlan Date: Mon, 1 May 2023 15:15:00 -0700 Subject: [PATCH 03/49] Tests for changes --- .../Managers/MealDetectionManagerTests.swift | 72 +++++++++++++++++-- 1 file changed, 65 insertions(+), 7 deletions(-) diff --git a/LoopTests/Managers/MealDetectionManagerTests.swift b/LoopTests/Managers/MealDetectionManagerTests.swift index d987e3cc3f..88b45fc984 100644 --- a/LoopTests/Managers/MealDetectionManagerTests.swift +++ b/LoopTests/Managers/MealDetectionManagerTests.swift @@ -12,6 +12,22 @@ import LoopCore import LoopKit @testable import Loop +fileprivate class MockGlucoseSample: GlucoseSampleValue { + let provenanceIdentifier = "" + let isDisplayOnly: Bool + let wasUserEntered: Bool + let condition: LoopKit.GlucoseCondition? = nil + let trendRate: HKQuantity? = nil + let quantity: HKQuantity = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 100) + let startDate: Date + + init(startDate: Date, isDisplayOnly: Bool = false, wasUserEntered: Bool = false) { + self.startDate = startDate + self.isDisplayOnly = isDisplayOnly + self.wasUserEntered = wasUserEntered + } +} + enum MissedMealTestType { private static var dateFormatter = ISO8601DateFormatter.localTimeDate() @@ -160,6 +176,8 @@ class MealDetectionManagerTests: XCTestCase { var bolusUnits: Double? var bolusDurationEstimator: ((Double) -> TimeInterval?)! + fileprivate var glucoseSamples: [MockGlucoseSample]! + @discardableResult func setUp(for testType: MissedMealTestType) -> [GlucoseEffectVelocity] { let healthStore = HKHealthStoreMock() @@ -198,6 +216,8 @@ class MealDetectionManagerTests: XCTestCase { test_currentDate: testType.currentDate ) + glucoseSamples = [MockGlucoseSample(startDate: now)] + bolusDurationEstimator = { units in self.bolusUnits = units return self.pumpManager.estimatedDuration(toBolus: units) @@ -252,7 +272,7 @@ class MealDetectionManagerTests: XCTestCase { let updateGroup = DispatchGroup() updateGroup.enter() - mealDetectionManager.hasMissedMeal(insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in + mealDetectionManager.hasMissedMeal(glucoseSamples: glucoseSamples, insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in XCTAssertEqual(status, .noMissedMeal) updateGroup.leave() } @@ -264,7 +284,7 @@ class MealDetectionManagerTests: XCTestCase { let updateGroup = DispatchGroup() updateGroup.enter() - mealDetectionManager.hasMissedMeal(insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in + mealDetectionManager.hasMissedMeal(glucoseSamples: glucoseSamples, insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in XCTAssertEqual(status, .noMissedMeal) updateGroup.leave() } @@ -277,7 +297,7 @@ class MealDetectionManagerTests: XCTestCase { let updateGroup = DispatchGroup() updateGroup.enter() - mealDetectionManager.hasMissedMeal(insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in + mealDetectionManager.hasMissedMeal(glucoseSamples: glucoseSamples, insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in XCTAssertEqual(status, .hasMissedMeal(startTime: testType.missedMealDate!, carbAmount: 55)) updateGroup.leave() } @@ -290,7 +310,7 @@ class MealDetectionManagerTests: XCTestCase { let updateGroup = DispatchGroup() updateGroup.enter() - mealDetectionManager.hasMissedMeal(insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in + mealDetectionManager.hasMissedMeal(glucoseSamples: glucoseSamples, insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in XCTAssertEqual(status, .hasMissedMeal(startTime: testType.missedMealDate!, carbAmount: 25)) updateGroup.leave() } @@ -303,7 +323,7 @@ class MealDetectionManagerTests: XCTestCase { let updateGroup = DispatchGroup() updateGroup.enter() - mealDetectionManager.hasMissedMeal(insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in + mealDetectionManager.hasMissedMeal(glucoseSamples: glucoseSamples, insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in XCTAssertEqual(status, .hasMissedMeal(startTime: testType.missedMealDate!, carbAmount: 50)) updateGroup.leave() } @@ -315,7 +335,7 @@ class MealDetectionManagerTests: XCTestCase { let updateGroup = DispatchGroup() updateGroup.enter() - mealDetectionManager.hasMissedMeal(insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in + mealDetectionManager.hasMissedMeal(glucoseSamples: glucoseSamples, insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in XCTAssertEqual(status, .noMissedMeal) updateGroup.leave() } @@ -328,7 +348,7 @@ class MealDetectionManagerTests: XCTestCase { let updateGroup = DispatchGroup() updateGroup.enter() - mealDetectionManager.hasMissedMeal(insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in + mealDetectionManager.hasMissedMeal(glucoseSamples: glucoseSamples, insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in XCTAssertEqual(status, .hasMissedMeal(startTime: testType.missedMealDate!, carbAmount: 40)) updateGroup.leave() } @@ -436,6 +456,44 @@ class MealDetectionManagerTests: XCTestCase { XCTAssertEqual(bolusUnits, 4.5) XCTAssertEqual(mealDetectionManager.lastMissedMealNotification?.deliveryTime, expectedDeliveryTime2) } + + func testHasCalibrationPoints_NoNotification() { + let testType = MissedMealTestType.manyMeals + let counteractionEffects = setUp(for: testType) + + let calibratedGlucoseSamples = [MockGlucoseSample(startDate: now), MockGlucoseSample(startDate: now, isDisplayOnly: true)] + + let updateGroup = DispatchGroup() + updateGroup.enter() + mealDetectionManager.hasMissedMeal(glucoseSamples: calibratedGlucoseSamples, insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in + XCTAssertEqual(status, .noMissedMeal) + updateGroup.leave() + } + updateGroup.wait() + + let manualGlucoseSamples = [MockGlucoseSample(startDate: now), MockGlucoseSample(startDate: now, wasUserEntered: true)] + updateGroup.enter() + mealDetectionManager.hasMissedMeal(glucoseSamples: manualGlucoseSamples, insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in + XCTAssertEqual(status, .noMissedMeal) + updateGroup.leave() + } + updateGroup.wait() + } + + func testHasTooOldCalibrationPoint_NoImpactOnNotificationDelivery() { + let testType = MissedMealTestType.manyMeals + let counteractionEffects = setUp(for: testType) + + let tooOldCalibratedGlucoseSamples = [MockGlucoseSample(startDate: now, isDisplayOnly: false), MockGlucoseSample(startDate: now.addingTimeInterval(-MissedMealSettings.maxRecency-1), isDisplayOnly: true)] + + let updateGroup = DispatchGroup() + updateGroup.enter() + mealDetectionManager.hasMissedMeal(glucoseSamples: tooOldCalibratedGlucoseSamples, insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in + XCTAssertEqual(status, .hasMissedMeal(startTime: testType.missedMealDate!, carbAmount: 40)) + updateGroup.leave() + } + updateGroup.wait() + } } extension MealDetectionManagerTests { From da11ae9745c4675dc01c0b9c0c92fca43091b96f Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Thu, 17 Aug 2023 16:02:04 -0500 Subject: [PATCH 04/49] StoredCarbEntry constructor signature change --- ...osingDecisionStore+SimulatedCoreData.swift | 12 ++++----- Loop/Views/SimpleBolusView.swift | 4 +-- .../ViewModels/BolusEntryViewModelTests.swift | 26 +++++++++++++++++-- .../SimpleBolusViewModelTests.swift | 4 +-- 4 files changed, 34 insertions(+), 12 deletions(-) diff --git a/Loop/Extensions/DosingDecisionStore+SimulatedCoreData.swift b/Loop/Extensions/DosingDecisionStore+SimulatedCoreData.swift index 0e3dbe44fb..558d634539 100644 --- a/Loop/Extensions/DosingDecisionStore+SimulatedCoreData.swift +++ b/Loop/Extensions/DosingDecisionStore+SimulatedCoreData.swift @@ -103,23 +103,23 @@ fileprivate extension StoredDosingDecision { historicalGlucose.append(HistoricalGlucoseValue(startDate: date.addingTimeInterval(.minutes(minutes)), quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 125 + minutes / 5))) } - let originalCarbEntry = StoredCarbEntry(uuid: UUID(uuidString: "C86DEB61-68E9-464E-9DD5-96A9CB445FD3")!, + let originalCarbEntry = StoredCarbEntry(startDate: date.addingTimeInterval(-.minutes(15)), + quantity: HKQuantity(unit: .gram(), doubleValue: 15), + uuid: UUID(uuidString: "C86DEB61-68E9-464E-9DD5-96A9CB445FD3")!, provenanceIdentifier: Bundle.main.bundleIdentifier!, syncIdentifier: "2B03D96C-6F5D-4140-99CD-80C3E64D6010", syncVersion: 1, - startDate: date.addingTimeInterval(-.minutes(15)), - quantity: HKQuantity(unit: .gram(), doubleValue: 15), foodType: "Simulated", absorptionTime: .hours(3), createdByCurrentApp: true, userCreatedDate: date.addingTimeInterval(-.minutes(15)), userUpdatedDate: date.addingTimeInterval(-.minutes(1))) - let carbEntry = StoredCarbEntry(uuid: UUID(uuidString: "71B699D7-0E8F-4B13-B7A1-E7751EB78E74")!, + let carbEntry = StoredCarbEntry(startDate: date.addingTimeInterval(-.minutes(1)), + quantity: HKQuantity(unit: .gram(), doubleValue: 25), + uuid: UUID(uuidString: "71B699D7-0E8F-4B13-B7A1-E7751EB78E74")!, provenanceIdentifier: Bundle.main.bundleIdentifier!, syncIdentifier: "2B03D96C-6F5D-4140-99CD-80C3E64D6010", syncVersion: 2, - startDate: date.addingTimeInterval(-.minutes(1)), - quantity: HKQuantity(unit: .gram(), doubleValue: 25), foodType: "Simulated", absorptionTime: .hours(5), createdByCurrentApp: true, diff --git a/Loop/Views/SimpleBolusView.swift b/Loop/Views/SimpleBolusView.swift index af9098abb6..2a7fc3fe59 100644 --- a/Loop/Views/SimpleBolusView.swift +++ b/Loop/Views/SimpleBolusView.swift @@ -369,12 +369,12 @@ struct SimpleBolusCalculatorView_Previews: PreviewProvider { func addCarbEntry(_ carbEntry: NewCarbEntry, replacing replacingEntry: StoredCarbEntry?, completion: @escaping (Result) -> Void) { let storedCarbEntry = StoredCarbEntry( + startDate: carbEntry.startDate, + quantity: carbEntry.quantity, uuid: UUID(), provenanceIdentifier: UUID().uuidString, syncIdentifier: UUID().uuidString, syncVersion: 1, - startDate: carbEntry.startDate, - quantity: carbEntry.quantity, foodType: carbEntry.foodType, absorptionTime: carbEntry.absorptionTime, createdByCurrentApp: true, diff --git a/LoopTests/ViewModels/BolusEntryViewModelTests.swift b/LoopTests/ViewModels/BolusEntryViewModelTests.swift index 787deb1834..2877cb257b 100644 --- a/LoopTests/ViewModels/BolusEntryViewModelTests.swift +++ b/LoopTests/ViewModels/BolusEntryViewModelTests.swift @@ -58,9 +58,31 @@ class BolusEntryViewModelTests: XCTestCase { fileprivate var delegate: MockBolusEntryViewModelDelegate! var now: Date = BolusEntryViewModelTests.now - let mockOriginalCarbEntry = StoredCarbEntry(uuid: UUID(), provenanceIdentifier: "provenanceIdentifier", syncIdentifier: "syncIdentifier", syncVersion: 0, startDate: BolusEntryViewModelTests.exampleStartDate, quantity: BolusEntryViewModelTests.exampleCarbQuantity, foodType: "foodType", absorptionTime: 1, createdByCurrentApp: true, userCreatedDate: BolusEntryViewModelTests.now, userUpdatedDate: BolusEntryViewModelTests.now) + let mockOriginalCarbEntry = StoredCarbEntry( + startDate: BolusEntryViewModelTests.exampleStartDate, + quantity: BolusEntryViewModelTests.exampleCarbQuantity, + uuid: UUID(), + provenanceIdentifier: "provenanceIdentifier", + syncIdentifier: "syncIdentifier", + syncVersion: 0, + foodType: "foodType", + absorptionTime: 1, + createdByCurrentApp: true, + userCreatedDate: BolusEntryViewModelTests.now, + userUpdatedDate: BolusEntryViewModelTests.now) let mockPotentialCarbEntry = NewCarbEntry(quantity: BolusEntryViewModelTests.exampleCarbQuantity, startDate: BolusEntryViewModelTests.exampleStartDate, foodType: "foodType", absorptionTime: 1) - let mockFinalCarbEntry = StoredCarbEntry(uuid: UUID(), provenanceIdentifier: "provenanceIdentifier", syncIdentifier: "syncIdentifier", syncVersion: 1, startDate: BolusEntryViewModelTests.exampleStartDate, quantity: BolusEntryViewModelTests.exampleCarbQuantity, foodType: "foodType", absorptionTime: 1, createdByCurrentApp: true, userCreatedDate: BolusEntryViewModelTests.now, userUpdatedDate: BolusEntryViewModelTests.now) + let mockFinalCarbEntry = StoredCarbEntry( + startDate: BolusEntryViewModelTests.exampleStartDate, + quantity: BolusEntryViewModelTests.exampleCarbQuantity, + uuid: UUID(), + provenanceIdentifier: "provenanceIdentifier", + syncIdentifier: "syncIdentifier", + syncVersion: 1, + foodType: "foodType", + absorptionTime: 1, + createdByCurrentApp: true, + userCreatedDate: BolusEntryViewModelTests.now, + userUpdatedDate: BolusEntryViewModelTests.now) let mockUUID = BolusEntryViewModelTests.mockUUID.uuidString let queue = DispatchQueue(label: "BolusEntryViewModelTests") var saveAndDeliverSuccess = false diff --git a/LoopTests/ViewModels/SimpleBolusViewModelTests.swift b/LoopTests/ViewModels/SimpleBolusViewModelTests.swift index 7aab8112f4..94c1fd8661 100644 --- a/LoopTests/ViewModels/SimpleBolusViewModelTests.swift +++ b/LoopTests/ViewModels/SimpleBolusViewModelTests.swift @@ -294,12 +294,12 @@ extension SimpleBolusViewModelTests: SimpleBolusViewModelDelegate { addedCarbEntry = carbEntry let storedCarbEntry = StoredCarbEntry( + startDate: carbEntry.startDate, + quantity: carbEntry.quantity, uuid: UUID(), provenanceIdentifier: UUID().uuidString, syncIdentifier: UUID().uuidString, syncVersion: 1, - startDate: carbEntry.startDate, - quantity: carbEntry.quantity, foodType: carbEntry.foodType, absorptionTime: carbEntry.absorptionTime, createdByCurrentApp: true, From dae9b622a28b563db0cb2c06191fcb57bfcb57c2 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Mon, 21 Aug 2023 18:05:46 -0700 Subject: [PATCH 05/49] Overwrite profile_expire_date for GitHub built --- Scripts/capture-build-details.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Scripts/capture-build-details.sh b/Scripts/capture-build-details.sh index 66f827d7c3..de8a438b96 100755 --- a/Scripts/capture-build-details.sh +++ b/Scripts/capture-build-details.sh @@ -89,5 +89,14 @@ then if [ -n "$branch" ]; then plutil -replace com-loopkit-LoopWorkspace-git-branch -string "${branch}" "${info_plist_path}" fi + # determine if this is a GitHub Action build (with 90 day expiration) + folderName=$(pwd) + runnerString="/Users/runner" + if [ "${folderName:0:13}" == "$runnerString" ]; then + # overwrite profile_expire_date + profile_expire_date=$(date -j -v+90d +"%Y-%m-%dT%H:%M:%SZ") + echo "runnerString detected, update profile_expire_date to ${profile_expire_date}" + plutil -replace com-loopkit-Loop-profile-expiration -date "${profile_expire_date}" "${info_plist_path}" + fi popd . > /dev/null fi From 3a8d2c9d3575c3695791eb9508c59982fdc1633c Mon Sep 17 00:00:00 2001 From: Cameron Ingham Date: Mon, 21 Aug 2023 23:08:25 -0700 Subject: [PATCH 06/49] [LOOP-4652] Replace AnyView --- .../StatusTableViewController.swift | 50 ++++++++++++++----- ...icationsCriticalAlertPermissionsView.swift | 4 +- Loop/Views/SettingsView.swift | 48 +++++++++--------- LoopTests/Managers/SupportManagerTests.swift | 6 +-- .../DismissibleHostingController.swift | 4 +- 5 files changed, 69 insertions(+), 43 deletions(-) diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index 74b49790aa..3e5312dafb 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -1385,22 +1385,46 @@ final class StatusTableViewController: LoopChartsTableViewController { @IBAction func presentBolusScreen() { presentBolusEntryView() } - - func presentBolusEntryView(enableManualGlucoseEntry: Bool = false) { - let hostingController: DismissibleHostingController + + @ViewBuilder + func bolusEntryView(enableManualGlucoseEntry: Bool = false) -> some View { if FeatureFlags.simpleBolusCalculatorEnabled && !automaticDosingStatus.automaticDosingEnabled { - let viewModel = SimpleBolusViewModel(delegate: deviceManager, displayMealEntry: false) - let bolusEntryView = SimpleBolusView(viewModel: viewModel).environmentObject(deviceManager.displayGlucosePreference) - hostingController = DismissibleHostingController(rootView: bolusEntryView, isModalInPresentation: false) + SimpleBolusView( + viewModel: SimpleBolusViewModel( + delegate: deviceManager, + displayMealEntry: false + ) + ) + .environmentObject(deviceManager.displayGlucosePreference) } else { - let viewModel = BolusEntryViewModel(delegate: deviceManager, screenWidth: UIScreen.main.bounds.width, isManualGlucoseEntryEnabled: enableManualGlucoseEntry) - Task { @MainActor in - await viewModel.generateRecommendationAndStartObserving() - } - viewModel.analyticsServicesManager = deviceManager.analyticsServicesManager - let bolusEntryView = BolusEntryView(viewModel: viewModel).environmentObject(deviceManager.displayGlucosePreference) - hostingController = DismissibleHostingController(rootView: bolusEntryView, isModalInPresentation: false) + let viewModel: BolusEntryViewModel = { + let viewModel = BolusEntryViewModel( + delegate: deviceManager, + screenWidth: UIScreen.main.bounds.width, + isManualGlucoseEntryEnabled: enableManualGlucoseEntry + ) + + Task { @MainActor in + await viewModel.generateRecommendationAndStartObserving() + } + + viewModel.analyticsServicesManager = deviceManager.analyticsServicesManager + + return viewModel + }() + + BolusEntryView(viewModel: viewModel) + .environmentObject(deviceManager.displayGlucosePreference) } + } + + func presentBolusEntryView(enableManualGlucoseEntry: Bool = false) { + let hostingController = DismissibleHostingController( + content: bolusEntryView( + enableManualGlucoseEntry: enableManualGlucoseEntry + ) + ) + let navigationWrapper = UINavigationController(rootViewController: hostingController) hostingController.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: navigationWrapper, action: #selector(dismissWithAnimation)) present(navigationWrapper, animated: true) diff --git a/Loop/Views/NotificationsCriticalAlertPermissionsView.swift b/Loop/Views/NotificationsCriticalAlertPermissionsView.swift index e69084a46a..b9e1552036 100644 --- a/Loop/Views/NotificationsCriticalAlertPermissionsView.swift +++ b/Loop/Views/NotificationsCriticalAlertPermissionsView.swift @@ -32,8 +32,8 @@ public struct NotificationsCriticalAlertPermissionsView: View { public var body: some View { switch mode { - case .flow: return AnyView(content()) - case .topLevel: return AnyView(navigationContent()) + case .flow: content() + case .topLevel: navigationContent() } } diff --git a/Loop/Views/SettingsView.swift b/Loop/Views/SettingsView.swift index 0b4cb55133..7eadb7f1ec 100644 --- a/Loop/Views/SettingsView.swift +++ b/Loop/Views/SettingsView.swift @@ -119,13 +119,13 @@ extension String: Identifiable { } } -struct PluginMenuItem: Identifiable { +struct PluginMenuItem: Identifiable { var id: String { return pluginIdentifier + String(describing: offset) } let section: SettingsMenuSection - let view: AnyView + let view: Content let pluginIdentifier: String let offset: Int } @@ -206,7 +206,7 @@ extension SettingsView { Section(header: SectionHeader(label: NSLocalizedString("Configuration", comment: "The title of the Configuration section in settings"))) { LargeButton(action: { self.therapySettingsIsPresented = true }, includeArrow: true, - imageView: AnyView(Image("Therapy Icon")), + imageView: Image("Therapy Icon"), label: NSLocalizedString("Therapy Settings", comment: "Title text for button to Therapy Settings"), descriptiveText: NSLocalizedString("Diabetes Treatment", comment: "Descriptive text for Therapy Settings")) .sheet(isPresented: $therapySettingsIsPresented) { @@ -235,7 +235,7 @@ extension SettingsView { } } - private var pluginMenuItems: [PluginMenuItem] { + private var pluginMenuItems: [PluginMenuItem] { self.viewModel.availableSupports.flatMap { plugin in plugin.configurationMenuItems().enumerated().map { index, item in PluginMenuItem(section: item.section, view: item.view, pluginIdentifier: plugin.identifier, offset: index) @@ -261,7 +261,7 @@ extension SettingsView { } else if viewModel.isOnboardingComplete { LargeButton(action: { self.pumpChooserIsPresented = true }, includeArrow: false, - imageView: AnyView(plusImage), + imageView: plusImage, label: NSLocalizedString("Add Pump", comment: "Title text for button to add pump device"), descriptiveText: NSLocalizedString("Tap here to set up a pump", comment: "Descriptive text for button to add pump device")) .actionSheet(isPresented: $pumpChooserIsPresented) { @@ -293,7 +293,7 @@ extension SettingsView { } else { LargeButton(action: { self.cgmChooserIsPresented = true }, includeArrow: false, - imageView: AnyView(plusImage), + imageView: plusImage, label: NSLocalizedString("Add CGM", comment: "Title text for button to add CGM device"), descriptiveText: NSLocalizedString("Tap here to set up a CGM", comment: "Descriptive text for button to add CGM device")) .actionSheet(isPresented: $cgmChooserIsPresented) { @@ -306,7 +306,7 @@ extension SettingsView { Section { LargeButton(action: { self.favoriteFoodsIsPresented = true }, includeArrow: true, - imageView: AnyView(Image("Favorite Foods Icon").renderingMode(.template).foregroundColor(carbTintColor)), + imageView: Image("Favorite Foods Icon").renderingMode(.template).foregroundColor(carbTintColor), label: "Favorite Foods", descriptiveText: "Simplify Carb Entry") } @@ -339,7 +339,7 @@ extension SettingsView { if viewModel.servicesViewModel.inactiveServices().count > 0 { LargeButton(action: { self.serviceChooserIsPresented = true }, includeArrow: false, - imageView: AnyView(plusImage), + imageView: plusImage, label: NSLocalizedString("Add Service", comment: "The title of the add service button in settings"), descriptiveText: NSLocalizedString("Tap here to set up a Service", comment: "The descriptive text of the add service button in settings")) .actionSheet(isPresented: $serviceChooserIsPresented) { @@ -455,41 +455,43 @@ extension SettingsView { .padding(EdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)) } - private func deviceImage(uiImage: UIImage?) -> AnyView { + @ViewBuilder + private func deviceImage(uiImage: UIImage?) -> some View { if let uiImage = uiImage { - return AnyView(Image(uiImage: uiImage) + Image(uiImage: uiImage) .renderingMode(.original) .resizable() - .scaledToFit()) + .scaledToFit() } else { - return AnyView(Spacer()) + Spacer() } } - private func serviceImage(uiImage: UIImage?) -> AnyView { - return deviceImage(uiImage: uiImage) + @ViewBuilder + private func serviceImage(uiImage: UIImage?) -> some View { + deviceImage(uiImage: uiImage) } } -fileprivate struct LargeButton: View { +fileprivate struct LargeButton: View { let action: () -> Void var includeArrow: Bool = true - let imageView: AnyView + let imageView: Content let label: String let descriptiveText: String // TODO: The design doesn't show this, but do we need to consider different values here for different size classes? - static let spacing: CGFloat = 15 - static let imageWidth: CGFloat = 60 - static let imageHeight: CGFloat = 60 - static let topBottomPadding: CGFloat = 10 + private let spacing: CGFloat = 15 + private let imageWidth: CGFloat = 60 + private let imageHeight: CGFloat = 60 + private let topBottomPadding: CGFloat = 10 public var body: some View { Button(action: action) { HStack { - HStack(spacing: Self.spacing) { - imageView.frame(width: Self.imageWidth, height: Self.imageHeight) + HStack(spacing: spacing) { + imageView.frame(width: imageWidth, height: imageHeight) VStack(alignment: .leading) { Text(label) .foregroundColor(.primary) @@ -502,7 +504,7 @@ fileprivate struct LargeButton: View { Image(systemName: "chevron.right").foregroundColor(.gray).font(.footnote) } } - .padding(EdgeInsets(top: Self.topBottomPadding, leading: 0, bottom: Self.topBottomPadding, trailing: 0)) + .padding(EdgeInsets(top: topBottomPadding, leading: 0, bottom: topBottomPadding, trailing: 0)) } } } diff --git a/LoopTests/Managers/SupportManagerTests.swift b/LoopTests/Managers/SupportManagerTests.swift index f245549573..ac0d42b512 100644 --- a/LoopTests/Managers/SupportManagerTests.swift +++ b/LoopTests/Managers/SupportManagerTests.swift @@ -16,9 +16,9 @@ class SupportManagerTests: XCTestCase { enum MockError: Error { case nothing } class Mixin { - func supportMenuItem(supportInfoProvider: SupportInfoProvider, urlHandler: @escaping (URL) -> Void) -> AnyView? { - nil - } + @ViewBuilder + func supportMenuItem(supportInfoProvider: SupportInfoProvider, urlHandler: @escaping (URL) -> Void) -> some View {} + func softwareUpdateView(bundleIdentifier: String, currentVersion: String, guidanceColors: GuidanceColors, openAppStore: (() -> Void)?) -> AnyView? { nil } diff --git a/LoopUI/Extensions/DismissibleHostingController.swift b/LoopUI/Extensions/DismissibleHostingController.swift index ce0bab23b9..47670f1b27 100644 --- a/LoopUI/Extensions/DismissibleHostingController.swift +++ b/LoopUI/Extensions/DismissibleHostingController.swift @@ -10,13 +10,13 @@ import SwiftUI import LoopKitUI extension DismissibleHostingController { - public convenience init( + public convenience init( rootView: Content, dismissalMode: DismissalMode = .modalDismiss, isModalInPresentation: Bool = true, onDisappear: @escaping () -> Void = {} ) { - self.init(rootView: rootView, + self.init(content: rootView, dismissalMode: dismissalMode, isModalInPresentation: isModalInPresentation, onDisappear: onDisappear, From c68d2cc2f7521678b18bc3cb17a9f53aa5f36442 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Tue, 22 Aug 2023 15:48:22 -0700 Subject: [PATCH 07/49] use GITHUB_ACTION to update profile expire date --- Scripts/capture-build-details.sh | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/Scripts/capture-build-details.sh b/Scripts/capture-build-details.sh index de8a438b96..ea7b1f27f8 100755 --- a/Scripts/capture-build-details.sh +++ b/Scripts/capture-build-details.sh @@ -72,6 +72,17 @@ if [ -e "${provisioning_profile_path}" ]; then profile_expire_date=$(security cms -D -i "${provisioning_profile_path}" | plutil -p - | grep ExpirationDate | cut -b 23-) # Convert to plutil format profile_expire_date=$(date -j -f "%Y-%m-%d %H:%M:%S" "${profile_expire_date}" +"%Y-%m-%dT%H:%M:%SZ") + # Handle github action, testflight builds that expire <= 90 days + if [ -n "$GITHUB_ACTIONS" ]; then + github_expire_date=$(date -j -v+90d +"%Y-%m-%dT%H:%M:%SZ") + + if [ "$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "${github_expire_date}" +%s)" -lt "$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "${profile_expire_date}" +%s)" ]; then + profile_expire_date=$github_expire_date + else + echo "GitHub Actions detected, expiration date is not more than 90 days in the future." + fi + fi + plutil -replace com-loopkit-Loop-profile-expiration -date "${profile_expire_date}" "${info_plist_path}" else warn "Invalid provisioning profile path ${provisioning_profile_path}" @@ -89,14 +100,5 @@ then if [ -n "$branch" ]; then plutil -replace com-loopkit-LoopWorkspace-git-branch -string "${branch}" "${info_plist_path}" fi - # determine if this is a GitHub Action build (with 90 day expiration) - folderName=$(pwd) - runnerString="/Users/runner" - if [ "${folderName:0:13}" == "$runnerString" ]; then - # overwrite profile_expire_date - profile_expire_date=$(date -j -v+90d +"%Y-%m-%dT%H:%M:%SZ") - echo "runnerString detected, update profile_expire_date to ${profile_expire_date}" - plutil -replace com-loopkit-Loop-profile-expiration -date "${profile_expire_date}" "${info_plist_path}" - fi popd . > /dev/null fi From ed4d1e8611edaa70e73ba99bd8014ebbc200eff6 Mon Sep 17 00:00:00 2001 From: SwiftlyNoah Date: Tue, 22 Aug 2023 20:54:53 -0400 Subject: [PATCH 08/49] Override warning fix --- Loop/View Models/CarbEntryViewModel.swift | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Loop/View Models/CarbEntryViewModel.swift b/Loop/View Models/CarbEntryViewModel.swift index d04ddba78e..f271793d1c 100644 --- a/Loop/View Models/CarbEntryViewModel.swift +++ b/Loop/View Models/CarbEntryViewModel.swift @@ -290,13 +290,14 @@ final class CarbEntryViewModel: ObservableObject { } private func checkIfOverrideEnabled() { - if let managerSettings = delegate?.settings { - if let overrideSettings = managerSettings.scheduleOverride?.settings, overrideSettings.effectiveInsulinNeedsScaleFactor != 1.0 { - self.warnings.insert(.overrideInProgress) - } - else { - self.warnings.remove(.overrideInProgress) - } + if let managerSettings = delegate?.settings, + managerSettings.scheduleOverrideEnabled(at: Date()), + let overrideSettings = managerSettings.scheduleOverride?.settings, + overrideSettings.effectiveInsulinNeedsScaleFactor != 1.0 { + self.warnings.insert(.overrideInProgress) + } + else { + self.warnings.remove(.overrideInProgress) } } From 599ba109ef2fc11df51a5f3fbf47a74102c5d81c Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Fri, 25 Aug 2023 13:35:32 -0500 Subject: [PATCH 09/49] Adding test for Loop functional algorithm --- Loop.xcodeproj/project.pbxproj | 4 + LoopTests/Managers/LoopAlgorithmTests.swift | 137 ++++++++++++++++++++ LoopTests/Mock Stores/MockCarbStore.swift | 1 - 3 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 LoopTests/Managers/LoopAlgorithmTests.swift diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index aaa0a470ac..512f50cdf4 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -470,6 +470,7 @@ C1D0B6302986D4D90098D215 /* LocalizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D0B62F2986D4D90098D215 /* LocalizedString.swift */; }; C1D0B6312986D4D90098D215 /* LocalizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D0B62F2986D4D90098D215 /* LocalizedString.swift */; }; C1D289B522F90A52003FFBD9 /* BasalDeliveryState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D289B422F90A52003FFBD9 /* BasalDeliveryState.swift */; }; + C1D476B42A8ED179002C1C87 /* LoopAlgorithmTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D476B32A8ED179002C1C87 /* LoopAlgorithmTests.swift */; }; C1D6EEA02A06C7270047DE5C /* MKRingProgressView in Frameworks */ = {isa = PBXBuildFile; productRef = C1D6EE9F2A06C7270047DE5C /* MKRingProgressView */; }; C1DE5D23251BFC4D00439E49 /* SimpleBolusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1DE5D22251BFC4D00439E49 /* SimpleBolusView.swift */; }; C1E2773E224177C000354103 /* ClockKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C1E2773D224177C000354103 /* ClockKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; @@ -1543,6 +1544,7 @@ C1D197FE232CF92D0096D646 /* capture-build-details.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = "capture-build-details.sh"; sourceTree = ""; }; C1D289B422F90A52003FFBD9 /* BasalDeliveryState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasalDeliveryState.swift; sourceTree = ""; }; C1D70F7A2A914F71009FE129 /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = he.lproj/InfoPlist.strings; sourceTree = ""; }; + C1D476B32A8ED179002C1C87 /* LoopAlgorithmTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopAlgorithmTests.swift; sourceTree = ""; }; C1DA986B2843B6F9001D04CC /* PersistedProperty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistedProperty.swift; sourceTree = ""; }; C1DE5D22251BFC4D00439E49 /* SimpleBolusView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SimpleBolusView.swift; sourceTree = ""; }; C1E2773D224177C000354103 /* ClockKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ClockKit.framework; path = Platforms/WatchOS.platform/Developer/SDKs/WatchOS.sdk/System/Library/Frameworks/ClockKit.framework; sourceTree = DEVELOPER_DIR; }; @@ -1864,6 +1866,7 @@ E9B3552C293592B40076AB04 /* MealDetectionManagerTests.swift */, 1D70C40026EC0F9D00C62570 /* SupportManagerTests.swift */, A9F5F1F4251050EC00E7C8A4 /* ZipArchiveTests.swift */, + C1D476B32A8ED179002C1C87 /* LoopAlgorithmTests.swift */, ); path = Managers; sourceTree = ""; @@ -3991,6 +3994,7 @@ E93E86A824DDCC4400FF40C8 /* MockDoseStore.swift in Sources */, B4D4534128E5CA7900F1A8D9 /* AlertMuterTests.swift in Sources */, E98A55F124EDD85E0008715D /* MockDosingDecisionStore.swift in Sources */, + C1D476B42A8ED179002C1C87 /* LoopAlgorithmTests.swift in Sources */, 8968B114240C55F10074BB48 /* LoopSettingsTests.swift in Sources */, A9BD28E7272226B40071DF15 /* TestLocalizedError.swift in Sources */, A9F5F1F5251050EC00E7C8A4 /* ZipArchiveTests.swift in Sources */, diff --git a/LoopTests/Managers/LoopAlgorithmTests.swift b/LoopTests/Managers/LoopAlgorithmTests.swift new file mode 100644 index 0000000000..76745cb1dc --- /dev/null +++ b/LoopTests/Managers/LoopAlgorithmTests.swift @@ -0,0 +1,137 @@ +// +// LoopAlgorithmTests.swift +// LoopTests +// +// Created by Pete Schwamb on 8/17/23. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import XCTest +import LoopKit +import LoopCore +import HealthKit + +final class LoopAlgorithmTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + public var bundle: Bundle { + return Bundle(for: type(of: self)) + } + + public func loadFixture(_ resourceName: String) -> T { + let path = bundle.path(forResource: resourceName, ofType: "json")! + return try! JSONSerialization.jsonObject(with: Data(contentsOf: URL(fileURLWithPath: path)), options: []) as! T + } + + func loadBasalRateScheduleFixture(_ resourceName: String) -> BasalRateSchedule { + let fixture: [JSONDictionary] = loadFixture(resourceName) + + let items = fixture.map { + return RepeatingScheduleValue(startTime: TimeInterval(minutes: $0["minutes"] as! Double), value: $0["rate"] as! Double) + } + + return BasalRateSchedule(dailyItems: items, timeZone: .utcTimeZone)! + } + + func loadPredictedGlucoseFixture(_ name: String) -> [PredictedGlucoseValue] { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + + let url = bundle.url(forResource: name, withExtension: "json")! + return try! decoder.decode([PredictedGlucoseValue].self, from: try! Data(contentsOf: url)) + } + + + func testLiveCaptureWithFunctionalAlgorithm() throws { + // This matches the "testForecastFromLiveCaptureInputData" test of LoopDataManagerDosingTests, + // Using the same input data, but generating the forecast using LoopPrediction + + let mockGlucoseStore = MockGlucoseStore(for: .liveCapture) + let historicGlucose = mockGlucoseStore.storedGlucose! + + let mockDoseStore = MockDoseStore(for: .liveCapture) + let doses = mockDoseStore.doseHistory! + + let mockCarbStore = MockCarbStore(for: .liveCapture) + let carbEntries = mockCarbStore.carbHistory! + + let baseTime = historicGlucose.last!.startDate + let treatmentInterval = LoopAlgorithm.treatmentHistoryDateInterval(for: baseTime) + + + let isfStart = min(treatmentInterval.start, doses.map { $0.startDate }.min() ?? .distantFuture) + let isfEnd = baseTime.addingTimeInterval(InsulinMath.defaultInsulinActivityDuration).dateCeiledToTimeInterval(GlucoseMath.defaultDelta) + + let basalRateSchedule = loadBasalRateScheduleFixture("basal_profile") + + let insulinSensitivitySchedule = InsulinSensitivitySchedule( + unit: .milligramsPerDeciliter, + dailyItems: [ + RepeatingScheduleValue(startTime: 0, value: 45), + RepeatingScheduleValue(startTime: 32400, value: 55) + ], + timeZone: .utcTimeZone + )! + let carbRatioSchedule = CarbRatioSchedule( + unit: .gram(), + dailyItems: [ + RepeatingScheduleValue(startTime: 0.0, value: 10.0), + ], + timeZone: .utcTimeZone + )! + + let glucoseTargetRangeSchedule = GlucoseRangeSchedule( + unit: HKUnit.milligramsPerDeciliter, + dailyItems: [ + RepeatingScheduleValue(startTime: TimeInterval(0), value: DoubleRange(minValue: 100, maxValue: 110)), + RepeatingScheduleValue(startTime: TimeInterval(28800), value: DoubleRange(minValue: 90, maxValue: 100)), + RepeatingScheduleValue(startTime: TimeInterval(75600), value: DoubleRange(minValue: 100, maxValue: 110)) + ], + timeZone: .utcTimeZone)! + + let settings = LoopAlgorithmSettings( + basal: basalRateSchedule.between(start: treatmentInterval.start, end: treatmentInterval.end), + sensitivity: insulinSensitivitySchedule.quantitiesBetween(start: isfStart, end: isfEnd), + carbRatio: carbRatioSchedule.between(start: treatmentInterval.start, end: treatmentInterval.end), + target: glucoseTargetRangeSchedule.quantityBetween(start: baseTime, end: isfEnd), + maximumBasalRatePerHour: 5.0, + maximumBolus: 10, + suspendThreshold: GlucoseThreshold(unit: HKUnit.milligramsPerDeciliter, value: 75)) + + let input = LoopPredictionInput( + glucoseHistory: historicGlucose, + doses: doses, + carbEntries: carbEntries, + settings: settings) + + let prediction = try LoopAlgorithm.generatePrediction(input: input) + + let expectedPredictedGlucose = loadPredictedGlucoseFixture("live_capture_predicted_glucose") + + XCTAssertEqual(expectedPredictedGlucose.count, prediction.glucose.count) + + let defaultAccuracy = 1.0 / 40.0 + + for (expected, calculated) in zip(expectedPredictedGlucose, prediction.glucose) { + XCTAssertEqual(expected.startDate, calculated.startDate) + XCTAssertEqual(expected.quantity.doubleValue(for: .milligramsPerDeciliter), calculated.quantity.doubleValue(for: .milligramsPerDeciliter), accuracy: defaultAccuracy) + } + + //XCTAssertEqual(1.99, recommendedBasal!.unitsPerHour, accuracy: defaultAccuracy) + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/LoopTests/Mock Stores/MockCarbStore.swift b/LoopTests/Mock Stores/MockCarbStore.swift index 25537d2267..2c8d7407e7 100644 --- a/LoopTests/Mock Stores/MockCarbStore.swift +++ b/LoopTests/Mock Stores/MockCarbStore.swift @@ -174,5 +174,4 @@ extension MockCarbStore { return nil } } - } From 2b58a68e81e71986771be7273d3242182a1d85b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sun, 27 Aug 2023 21:16:31 +0200 Subject: [PATCH 10/49] Detection of github build, calculate date for testflight expire --- Common/Models/BuildDetails.swift | 4 + Loop.xcodeproj/project.pbxproj | 8 +- Loop/Managers/AppExpirationAlerter.swift | 134 +++++++++++++++++++ Loop/Managers/LoopAppManager.swift | 2 +- Loop/Managers/ProfileExpirationAlerter.swift | 86 ------------ Loop/Views/SettingsView.swift | 55 +++++--- Scripts/capture-build-details.sh | 15 +-- 7 files changed, 187 insertions(+), 117 deletions(-) create mode 100644 Loop/Managers/AppExpirationAlerter.swift delete mode 100644 Loop/Managers/ProfileExpirationAlerter.swift diff --git a/Common/Models/BuildDetails.swift b/Common/Models/BuildDetails.swift index 63517e7e79..0badb257ac 100644 --- a/Common/Models/BuildDetails.swift +++ b/Common/Models/BuildDetails.swift @@ -65,5 +65,9 @@ class BuildDetails { var workspaceGitBranch: String? { return dict["com-loopkit-LoopWorkspace-git-branch"] as? String } + + var isGitHubBuild: Bool? { + return dict["com-loopkit-GitHub-build"] as? Bool + } } diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index aaa0a470ac..ecba1c2d01 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -479,7 +479,7 @@ C1EF747228D6A44A00C8C083 /* CrashRecoveryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1EF747128D6A44A00C8C083 /* CrashRecoveryManager.swift */; }; C1F00C60285A802A006302C5 /* SwiftCharts in Frameworks */ = {isa = PBXBuildFile; productRef = C1F00C5F285A802A006302C5 /* SwiftCharts */; }; C1F00C78285A8256006302C5 /* SwiftCharts in Embed Frameworks */ = {isa = PBXBuildFile; productRef = C1F00C5F285A802A006302C5 /* SwiftCharts */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - C1F2075C26D6F9B0007AB7EB /* ProfileExpirationAlerter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F2075B26D6F9B0007AB7EB /* ProfileExpirationAlerter.swift */; }; + C1F2075C26D6F9B0007AB7EB /* AppExpirationAlerter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F2075B26D6F9B0007AB7EB /* AppExpirationAlerter.swift */; }; C1F7822627CC056900C0919A /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F7822527CC056900C0919A /* SettingsManager.swift */; }; C1F8B243223E73FD00DD66CF /* BolusProgressTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F8B1D122375E4200DD66CF /* BolusProgressTableViewCell.swift */; }; C1FB428C217806A400FAB378 /* StateColorPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FB428B217806A300FAB378 /* StateColorPalette.swift */; }; @@ -1565,7 +1565,7 @@ C1EB0D22299581D900628475 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/ckcomplication.strings; sourceTree = ""; }; C1EE9E802A38D0FB0064784A /* BuildDetails.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = BuildDetails.plist; sourceTree = ""; }; C1EF747128D6A44A00C8C083 /* CrashRecoveryManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashRecoveryManager.swift; sourceTree = ""; }; - C1F2075B26D6F9B0007AB7EB /* ProfileExpirationAlerter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileExpirationAlerter.swift; sourceTree = ""; }; + C1F2075B26D6F9B0007AB7EB /* AppExpirationAlerter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppExpirationAlerter.swift; sourceTree = ""; }; C1F48FF62995821600C8BD69 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = ""; }; C1F48FF72995821600C8BD69 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = ""; }; C1F48FF82995821600C8BD69 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; @@ -2307,7 +2307,7 @@ 1DA6499D2441266400F61E75 /* Alerts */, E95D37FF24EADE68005E2F50 /* Store Protocols */, E9B355232935906B0076AB04 /* Missed Meal Detection */, - C1F2075B26D6F9B0007AB7EB /* ProfileExpirationAlerter.swift */, + C1F2075B26D6F9B0007AB7EB /* AppExpirationAlerter.swift */, A96DAC2B2838F31200D94E38 /* SharedLogging.swift */, 7E69CFFB2A16A77E00203CBD /* ResetLoopManager.swift */, 84AA81E42A4A3981000B658B /* DeeplinkManager.swift */, @@ -3654,7 +3654,7 @@ C1D289B522F90A52003FFBD9 /* BasalDeliveryState.swift in Sources */, 4F2C15821E074FC600E160D4 /* NSTimeInterval.swift in Sources */, 4311FB9B1F37FE1B00D4C0A7 /* TitleSubtitleTextFieldTableViewCell.swift in Sources */, - C1F2075C26D6F9B0007AB7EB /* ProfileExpirationAlerter.swift in Sources */, + C1F2075C26D6F9B0007AB7EB /* AppExpirationAlerter.swift in Sources */, B4FEEF7D24B8A71F00A8DF9B /* DeviceDataManager+DeviceStatus.swift in Sources */, 142CB7592A60BF2E0075748A /* EditMode.swift in Sources */, E95D380324EADF36005E2F50 /* CarbStoreProtocol.swift in Sources */, diff --git a/Loop/Managers/AppExpirationAlerter.swift b/Loop/Managers/AppExpirationAlerter.swift new file mode 100644 index 0000000000..23ceb1e7a3 --- /dev/null +++ b/Loop/Managers/AppExpirationAlerter.swift @@ -0,0 +1,134 @@ +// +// AppExpirationAlerter.swift +// Loop +// +// Created by Pete Schwamb on 8/21/21. +// Copyright © 2021 LoopKit Authors. All rights reserved. +// + +import Foundation +import UserNotifications +import LoopCore + + +class AppExpirationAlerter { + + static let expirationAlertWindow: TimeInterval = .days(20) + static let settingsPageExpirationWarningModeWindow: TimeInterval = .days(3) + + static func alertIfNeeded(viewControllerToPresentFrom: UIViewController) { + + let now = Date() + + guard let profileExpiration = BuildDetails.default.profileExpiration, now > profileExpiration - expirationAlertWindow else { + return + } + + let expirationDate = calculateExpirationDate(profileExpiration: profileExpiration) + + let timeUntilExpiration = expirationDate.timeIntervalSince(now) + + let minimumTimeBetweenAlerts: TimeInterval = timeUntilExpiration > .hours(24) ? .days(2) : .hours(1) + + if let lastAlertDate = UserDefaults.appGroup?.lastProfileExpirationAlertDate { + guard now > lastAlertDate + minimumTimeBetweenAlerts else { + return + } + } + + let formatter = DateComponentsFormatter() + formatter.allowedUnits = [.day, .hour] + formatter.unitsStyle = .full + formatter.zeroFormattingBehavior = .dropLeading + formatter.maximumUnitCount = 1 + let timeUntilExpirationStr = formatter.string(from: timeUntilExpiration) + + let alertMessage = createVerboseAlertMessage(timeUntilExpirationStr: timeUntilExpirationStr!) + + var dialog: UIAlertController + if isTestFlightBuild() { + dialog = UIAlertController( + title: NSLocalizedString("TestFlight Expires Soon", comment: "The title for notification of upcoming TestFlight expiration"), + message: alertMessage, + preferredStyle: .alert) + dialog.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Text for ok action on notification of upcoming profile expiration"), style: .default, handler: nil)) + dialog.addAction(UIAlertAction(title: NSLocalizedString("More Info", comment: "Text for more info action on notification of upcoming TestFlight expiration"), style: .default, handler: { (_) in + UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/gh-actions/gh-update/")!) + })) + + } else { + dialog = UIAlertController( + title: NSLocalizedString("Profile Expires Soon", comment: "The title for notification of upcoming profile expiration"), + message: alertMessage, + preferredStyle: .alert) + dialog.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Text for ok action on notification of upcoming profile expiration"), style: .default, handler: nil)) + dialog.addAction(UIAlertAction(title: NSLocalizedString("More Info", comment: "Text for more info action on notification of upcoming profile expiration"), style: .default, handler: { (_) in + UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/build/updating/")!) + })) + } + viewControllerToPresentFrom.present(dialog, animated: true, completion: nil) + + UserDefaults.appGroup?.lastProfileExpirationAlertDate = now + } + + static func createVerboseAlertMessage(timeUntilExpirationStr:String) -> String { + if isTestFlightBuild() { + return String(format: NSLocalizedString("%1$@ will stop working in %2$@. You will need to update before that, with a new provisioning profile.", comment: "Format string for body for notification of upcoming provisioning profile expiration. (1: app name) (2: amount of time until expiration"), Bundle.main.bundleDisplayName, timeUntilExpirationStr) + } else { + return String(format: NSLocalizedString("%1$@ will stop working in %2$@. You will need to rebuild before that.", comment: "Format string for body for notification of upcoming provisioning profile expiration. (1: app name) (2: amount of time until expiration"), Bundle.main.bundleDisplayName, timeUntilExpirationStr) + } + } + + static func isNearExpiration(expirationDate:Date) -> Bool { + return expirationDate.timeIntervalSinceNow < settingsPageExpirationWarningModeWindow + } + + static func createProfileExpirationSettingsMessage(expirationDate:Date) -> String { + let nearExpiration = isNearExpiration(expirationDate: expirationDate) + let maxUnitCount = nearExpiration ? 2 : 1 // only include hours in the msg if near expiration + let readableRelativeTime: String? = relativeTimeFormatter(maxUnitCount: maxUnitCount).string(from: expirationDate.timeIntervalSinceNow) + let relativeTimeRemaining: String = readableRelativeTime ?? NSLocalizedString("Unknown time", comment: "Unknown amount of time in settings' profile expiration section") + let verboseMessage = createVerboseAlertMessage(timeUntilExpirationStr: relativeTimeRemaining) + let conciseMessage = relativeTimeRemaining + NSLocalizedString(" remaining", comment: "remaining time in setting's profile expiration section") + return nearExpiration ? verboseMessage : conciseMessage + } + + private static func relativeTimeFormatter(maxUnitCount:Int) -> DateComponentsFormatter { + let formatter = DateComponentsFormatter() + let includeHours = maxUnitCount == 2 + formatter.allowedUnits = includeHours ? [.day, .hour] : [.day] + formatter.unitsStyle = .full + formatter.zeroFormattingBehavior = .dropLeading + formatter.maximumUnitCount = maxUnitCount + return formatter; + } + + static func buildDate() -> Date? { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "EEE MMM d HH:mm:ss 'UTC' yyyy" + dateFormatter.locale = Locale(identifier: "en_US_POSIX") // Set locale to ensure parsing works + + guard let dateString = BuildDetails.default.buildDateString, + let date = dateFormatter.date(from: dateString) else { + return nil + } + + return date + } + + static func isTestFlightBuild() -> Bool { + return BuildDetails.default.isGitHubBuild ?? false + } + + static func calculateExpirationDate(profileExpiration: Date) -> Date { + let isTestFlight = isTestFlightBuild() + + if isTestFlight, let buildDate = buildDate() { + let testflightExpiration = Calendar.current.date(byAdding: .day, value: 90, to: buildDate)! + + return profileExpiration < testflightExpiration ? profileExpiration : testflightExpiration + } else { + return profileExpiration + } + } +} diff --git a/Loop/Managers/LoopAppManager.swift b/Loop/Managers/LoopAppManager.swift index 43c62d128b..9edf481ba2 100644 --- a/Loop/Managers/LoopAppManager.swift +++ b/Loop/Managers/LoopAppManager.swift @@ -323,7 +323,7 @@ class LoopAppManager: NSObject { func didBecomeActive() { if let rootViewController = rootViewController { - ProfileExpirationAlerter.alertIfNeeded(viewControllerToPresentFrom: rootViewController) + AppExpirationAlerter.alertIfNeeded(viewControllerToPresentFrom: rootViewController) } settingsManager?.didBecomeActive() deviceDataManager?.didBecomeActive() diff --git a/Loop/Managers/ProfileExpirationAlerter.swift b/Loop/Managers/ProfileExpirationAlerter.swift deleted file mode 100644 index 3aa742732b..0000000000 --- a/Loop/Managers/ProfileExpirationAlerter.swift +++ /dev/null @@ -1,86 +0,0 @@ -// -// ProfileExpirationAlerter.swift -// Loop -// -// Created by Pete Schwamb on 8/21/21. -// Copyright © 2021 LoopKit Authors. All rights reserved. -// - -import Foundation -import UserNotifications -import LoopCore - - -class ProfileExpirationAlerter { - - static let expirationAlertWindow: TimeInterval = .days(20) - static let settingsPageExpirationWarningModeWindow: TimeInterval = .days(3) - - static func alertIfNeeded(viewControllerToPresentFrom: UIViewController) { - - let now = Date() - - guard let profileExpiration = BuildDetails.default.profileExpiration, now > profileExpiration - expirationAlertWindow else { - return - } - - let timeUntilExpiration = profileExpiration.timeIntervalSince(now) - - let minimumTimeBetweenAlerts: TimeInterval = timeUntilExpiration > .hours(24) ? .days(2) : .hours(1) - - if let lastAlertDate = UserDefaults.appGroup?.lastProfileExpirationAlertDate { - guard now > lastAlertDate + minimumTimeBetweenAlerts else { - return - } - } - - let formatter = DateComponentsFormatter() - formatter.allowedUnits = [.day, .hour] - formatter.unitsStyle = .full - formatter.zeroFormattingBehavior = .dropLeading - formatter.maximumUnitCount = 1 - let timeUntilExpirationStr = formatter.string(from: timeUntilExpiration) - - let alertMessage = createVerboseAlertMessage(timeUntilExpirationStr: timeUntilExpirationStr!) - - let dialog = UIAlertController( - title: NSLocalizedString("Profile Expires Soon", comment: "The title for notification of upcoming profile expiration"), - message: alertMessage, - preferredStyle: .alert) - dialog.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Text for ok action on notification of upcoming profile expiration"), style: .default, handler: nil)) - dialog.addAction(UIAlertAction(title: NSLocalizedString("More Info", comment: "Text for more info action on notification of upcoming profile expiration"), style: .default, handler: { (_) in - UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/build/updating/")!) - })) - viewControllerToPresentFrom.present(dialog, animated: true, completion: nil) - - UserDefaults.appGroup?.lastProfileExpirationAlertDate = now - } - - static func createVerboseAlertMessage(timeUntilExpirationStr:String) -> String { - return String(format: NSLocalizedString("%1$@ will stop working in %2$@. You will need to update before that, with a new provisioning profile.", comment: "Format string for body for notification of upcoming provisioning profile expiration. (1: app name) (2: amount of time until expiration"), Bundle.main.bundleDisplayName, timeUntilExpirationStr) - } - - static func isNearProfileExpiration(profileExpiration:Date) -> Bool { - return profileExpiration.timeIntervalSinceNow < settingsPageExpirationWarningModeWindow - } - - static func createProfileExpirationSettingsMessage(profileExpiration:Date) -> String { - let nearExpiration = isNearProfileExpiration(profileExpiration: profileExpiration) - let maxUnitCount = nearExpiration ? 2 : 1 // only include hours in the msg if near expiration - let readableRelativeTime: String? = relativeTimeFormatter(maxUnitCount: maxUnitCount).string(from: profileExpiration.timeIntervalSinceNow) - let relativeTimeRemaining: String = readableRelativeTime ?? NSLocalizedString("Unknown time", comment: "Unknown amount of time in settings' profile expiration section") - let verboseMessage = createVerboseAlertMessage(timeUntilExpirationStr: relativeTimeRemaining) - let conciseMessage = relativeTimeRemaining + NSLocalizedString(" remaining", comment: "remaining time in setting's profile expiration section") - return nearExpiration ? verboseMessage : conciseMessage - } - - private static func relativeTimeFormatter(maxUnitCount:Int) -> DateComponentsFormatter { - let formatter = DateComponentsFormatter() - let includeHours = maxUnitCount == 2 - formatter.allowedUnits = includeHours ? [.day, .hour] : [.day] - formatter.unitsStyle = .full - formatter.zeroFormattingBehavior = .dropLeading - formatter.maximumUnitCount = maxUnitCount - return formatter; - } -} diff --git a/Loop/Views/SettingsView.swift b/Loop/Views/SettingsView.swift index 0b4cb55133..a07d623f40 100644 --- a/Loop/Views/SettingsView.swift +++ b/Loop/Views/SettingsView.swift @@ -417,25 +417,48 @@ extension SettingsView { DIY loop specific component to show users the amount of time remaining on their build before a rebuild is necessary. */ private func profileExpirationSection(profileExpiration:Date) -> some View { - let nearExpiration : Bool = ProfileExpirationAlerter.isNearProfileExpiration(profileExpiration: profileExpiration) - let profileExpirationMsg = ProfileExpirationAlerter.createProfileExpirationSettingsMessage(profileExpiration: profileExpiration) - let readableExpirationTime = Self.dateFormatter.string(from: profileExpiration) + let expirationDate = AppExpirationAlerter.calculateExpirationDate(profileExpiration: profileExpiration) + let isTestFlight = AppExpirationAlerter.isTestFlightBuild() - return Section(header: SectionHeader(label: NSLocalizedString("App Profile", comment: "Settings app profile section")), - footer: Text(NSLocalizedString("Profile expires ", comment: "Time that profile expires") + readableExpirationTime)) { - if(nearExpiration) { - Text(profileExpirationMsg).foregroundColor(.red) - } else { - HStack { - Text("Profile Expiration", comment: "Settings App Profile expiration view") - Spacer() - Text(profileExpirationMsg).foregroundColor(Color.secondary) + let nearExpiration : Bool = AppExpirationAlerter.isNearExpiration(expirationDate: expirationDate) + let profileExpirationMsg = AppExpirationAlerter.createProfileExpirationSettingsMessage(expirationDate: expirationDate) + let readableExpirationTime = Self.dateFormatter.string(from: expirationDate) + + if isTestFlight { + return Section(header: SectionHeader(label: NSLocalizedString("TestFlight", comment: "Settings app TestFlight section")), + footer: Text(NSLocalizedString("TestFlight expires ", comment: "Time that build expires") + readableExpirationTime)) { + if(nearExpiration) { + Text(profileExpirationMsg).foregroundColor(.red) + } else { + HStack { + Text("TestFlight Expiration", comment: "Settings TestFlight expiration view") + Spacer() + Text(profileExpirationMsg).foregroundColor(Color.secondary) + } + } + Button(action: { + UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/gh-actions/gh-update/")!) + }) { + Text(NSLocalizedString("How to update (LoopDocs)", comment: "The title text for how to update")) } } - Button(action: { - UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/build/updating/")!) - }) { - Text(NSLocalizedString("How to update (LoopDocs)", comment: "The title text for how to update")) + } else { + return Section(header: SectionHeader(label: NSLocalizedString("App Profile", comment: "Settings app profile section")), + footer: Text(NSLocalizedString("Profile expires ", comment: "Time that profile expires") + readableExpirationTime)) { + if(nearExpiration) { + Text(profileExpirationMsg).foregroundColor(.red) + } else { + HStack { + Text("Profile Expiration", comment: "Settings App Profile expiration view") + Spacer() + Text(profileExpirationMsg).foregroundColor(Color.secondary) + } + } + Button(action: { + UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/build/updating/")!) + }) { + Text(NSLocalizedString("How to update (LoopDocs)", comment: "The title text for how to update")) + } } } } diff --git a/Scripts/capture-build-details.sh b/Scripts/capture-build-details.sh index ea7b1f27f8..5431b4f239 100755 --- a/Scripts/capture-build-details.sh +++ b/Scripts/capture-build-details.sh @@ -72,16 +72,6 @@ if [ -e "${provisioning_profile_path}" ]; then profile_expire_date=$(security cms -D -i "${provisioning_profile_path}" | plutil -p - | grep ExpirationDate | cut -b 23-) # Convert to plutil format profile_expire_date=$(date -j -f "%Y-%m-%d %H:%M:%S" "${profile_expire_date}" +"%Y-%m-%dT%H:%M:%SZ") - # Handle github action, testflight builds that expire <= 90 days - if [ -n "$GITHUB_ACTIONS" ]; then - github_expire_date=$(date -j -v+90d +"%Y-%m-%dT%H:%M:%SZ") - - if [ "$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "${github_expire_date}" +%s)" -lt "$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "${profile_expire_date}" +%s)" ]; then - profile_expire_date=$github_expire_date - else - echo "GitHub Actions detected, expiration date is not more than 90 days in the future." - fi - fi plutil -replace com-loopkit-Loop-profile-expiration -date "${profile_expire_date}" "${info_plist_path}" else @@ -102,3 +92,8 @@ then fi popd . > /dev/null fi + +# Handle github action +if [ -n "$GITHUB_ACTIONS" ]; then + plutil -replace com-loopkit-GitHub-build -bool true "${info_plist_path}" +fi From 78a75cdf718e90f7ec9debb7e0b8ea0a99383bca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 28 Aug 2023 09:51:48 +0200 Subject: [PATCH 11/49] TF text adjustments --- Loop/Managers/AppExpirationAlerter.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Loop/Managers/AppExpirationAlerter.swift b/Loop/Managers/AppExpirationAlerter.swift index 23ceb1e7a3..08ff824d58 100644 --- a/Loop/Managers/AppExpirationAlerter.swift +++ b/Loop/Managers/AppExpirationAlerter.swift @@ -51,7 +51,7 @@ class AppExpirationAlerter { title: NSLocalizedString("TestFlight Expires Soon", comment: "The title for notification of upcoming TestFlight expiration"), message: alertMessage, preferredStyle: .alert) - dialog.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Text for ok action on notification of upcoming profile expiration"), style: .default, handler: nil)) + dialog.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Text for ok action on notification of upcoming TestFlight expiration"), style: .default, handler: nil)) dialog.addAction(UIAlertAction(title: NSLocalizedString("More Info", comment: "Text for more info action on notification of upcoming TestFlight expiration"), style: .default, handler: { (_) in UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/gh-actions/gh-update/")!) })) @@ -73,9 +73,9 @@ class AppExpirationAlerter { static func createVerboseAlertMessage(timeUntilExpirationStr:String) -> String { if isTestFlightBuild() { - return String(format: NSLocalizedString("%1$@ will stop working in %2$@. You will need to update before that, with a new provisioning profile.", comment: "Format string for body for notification of upcoming provisioning profile expiration. (1: app name) (2: amount of time until expiration"), Bundle.main.bundleDisplayName, timeUntilExpirationStr) + return String(format: NSLocalizedString("%1$@ will stop working in %2$@. You will need to rebuild before that.", comment: "Format string for body for notification of upcoming expiration. (1: app name) (2: amount of time until expiration"), Bundle.main.bundleDisplayName, timeUntilExpirationStr) } else { - return String(format: NSLocalizedString("%1$@ will stop working in %2$@. You will need to rebuild before that.", comment: "Format string for body for notification of upcoming provisioning profile expiration. (1: app name) (2: amount of time until expiration"), Bundle.main.bundleDisplayName, timeUntilExpirationStr) + return String(format: NSLocalizedString("%1$@ will stop working in %2$@. You will need to update before that, with a new provisioning profile.", comment: "Format string for body for notification of upcoming provisioning profile expiration. (1: app name) (2: amount of time until expiration"), Bundle.main.bundleDisplayName, timeUntilExpirationStr) } } From 874c393130a8f91c348e64675ec34485b44d17f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 28 Aug 2023 13:10:26 +0200 Subject: [PATCH 12/49] Fix for UTC timezone --- Loop/Managers/AppExpirationAlerter.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Loop/Managers/AppExpirationAlerter.swift b/Loop/Managers/AppExpirationAlerter.swift index 08ff824d58..6b8c0a88a2 100644 --- a/Loop/Managers/AppExpirationAlerter.swift +++ b/Loop/Managers/AppExpirationAlerter.swift @@ -107,6 +107,7 @@ class AppExpirationAlerter { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "EEE MMM d HH:mm:ss 'UTC' yyyy" dateFormatter.locale = Locale(identifier: "en_US_POSIX") // Set locale to ensure parsing works + dateFormatter.timeZone = TimeZone(identifier: "UTC") guard let dateString = BuildDetails.default.buildDateString, let date = dateFormatter.date(from: dateString) else { From 211a337a6a1324bd9a489f1b8fc43d5697170037 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Mon, 28 Aug 2023 12:49:20 -0700 Subject: [PATCH 13/49] fix for modal alert --- Loop/Managers/AppExpirationAlerter.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Loop/Managers/AppExpirationAlerter.swift b/Loop/Managers/AppExpirationAlerter.swift index 6b8c0a88a2..a637927f37 100644 --- a/Loop/Managers/AppExpirationAlerter.swift +++ b/Loop/Managers/AppExpirationAlerter.swift @@ -19,8 +19,8 @@ class AppExpirationAlerter { static func alertIfNeeded(viewControllerToPresentFrom: UIViewController) { let now = Date() - - guard let profileExpiration = BuildDetails.default.profileExpiration, now > profileExpiration - expirationAlertWindow else { + + guard let profileExpiration = BuildDetails.default.profileExpiration else { return } @@ -28,6 +28,10 @@ class AppExpirationAlerter { let timeUntilExpiration = expirationDate.timeIntervalSince(now) + if timeUntilExpiration > expirationAlertWindow { + return + } + let minimumTimeBetweenAlerts: TimeInterval = timeUntilExpiration > .hours(24) ? .days(2) : .hours(1) if let lastAlertDate = UserDefaults.appGroup?.lastProfileExpirationAlertDate { From b292371e9390c4caa5be5ad8f83b6689c8228edf Mon Sep 17 00:00:00 2001 From: marionbarker Date: Mon, 28 Aug 2023 14:42:37 -0700 Subject: [PATCH 14/49] test an alternative method for isTestFlightBuild --- Loop/Managers/AppExpirationAlerter.swift | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/Loop/Managers/AppExpirationAlerter.swift b/Loop/Managers/AppExpirationAlerter.swift index a637927f37..cbea4f4941 100644 --- a/Loop/Managers/AppExpirationAlerter.swift +++ b/Loop/Managers/AppExpirationAlerter.swift @@ -104,7 +104,7 @@ class AppExpirationAlerter { formatter.unitsStyle = .full formatter.zeroFormattingBehavior = .dropLeading formatter.maximumUnitCount = maxUnitCount - return formatter; + return formatter } static func buildDate() -> Date? { @@ -122,9 +122,23 @@ class AppExpirationAlerter { } static func isTestFlightBuild() -> Bool { - return BuildDetails.default.isGitHubBuild ?? false + // If an "embedded.mobileprovision" is present in the main bundle, then + // this is an Xcode, Ad-Hoc, or Enterprise distribution. Return false. + if Bundle.main.url(forResource: "embedded", withExtension: "mobileprovision") != nil { + return false + } + + // If an app store receipt is not present in the main bundle, then we cannot + // say whether this is a TestFlight or App Store distribution. Return false. + guard let receiptName = Bundle.main.appStoreReceiptURL?.lastPathComponent else { + return false + } + + // A TestFlight distribution presents a "sandboxReceipt", while an App Store + // distribution presents a "receipt". Return true if we have a TestFlight receipt. + return "sandboxReceipt".caseInsensitiveCompare(receiptName) == .orderedSame } - + static func calculateExpirationDate(profileExpiration: Date) -> Date { let isTestFlight = isTestFlightBuild() From 390938075e9e5e9524050dbe4c758e8f3a36141d Mon Sep 17 00:00:00 2001 From: marionbarker Date: Mon, 28 Aug 2023 15:42:34 -0700 Subject: [PATCH 15/49] Revert changes not needed by isTestFlightBuild function --- Common/Models/BuildDetails.swift | 4 ---- Scripts/capture-build-details.sh | 6 ------ 2 files changed, 10 deletions(-) diff --git a/Common/Models/BuildDetails.swift b/Common/Models/BuildDetails.swift index 0badb257ac..63517e7e79 100644 --- a/Common/Models/BuildDetails.swift +++ b/Common/Models/BuildDetails.swift @@ -65,9 +65,5 @@ class BuildDetails { var workspaceGitBranch: String? { return dict["com-loopkit-LoopWorkspace-git-branch"] as? String } - - var isGitHubBuild: Bool? { - return dict["com-loopkit-GitHub-build"] as? Bool - } } diff --git a/Scripts/capture-build-details.sh b/Scripts/capture-build-details.sh index 5431b4f239..66f827d7c3 100755 --- a/Scripts/capture-build-details.sh +++ b/Scripts/capture-build-details.sh @@ -72,7 +72,6 @@ if [ -e "${provisioning_profile_path}" ]; then profile_expire_date=$(security cms -D -i "${provisioning_profile_path}" | plutil -p - | grep ExpirationDate | cut -b 23-) # Convert to plutil format profile_expire_date=$(date -j -f "%Y-%m-%d %H:%M:%S" "${profile_expire_date}" +"%Y-%m-%dT%H:%M:%SZ") - plutil -replace com-loopkit-Loop-profile-expiration -date "${profile_expire_date}" "${info_plist_path}" else warn "Invalid provisioning profile path ${provisioning_profile_path}" @@ -92,8 +91,3 @@ then fi popd . > /dev/null fi - -# Handle github action -if [ -n "$GITHUB_ACTIONS" ]; then - plutil -replace com-loopkit-GitHub-build -bool true "${info_plist_path}" -fi From b6eb08beba45cfcfcab004f2999cbc0b2b420822 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Tue, 29 Aug 2023 16:15:42 -0700 Subject: [PATCH 16/49] Add simulator check for completeness --- Loop/Managers/AppExpirationAlerter.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Loop/Managers/AppExpirationAlerter.swift b/Loop/Managers/AppExpirationAlerter.swift index cbea4f4941..e7768f92b6 100644 --- a/Loop/Managers/AppExpirationAlerter.swift +++ b/Loop/Managers/AppExpirationAlerter.swift @@ -122,6 +122,12 @@ class AppExpirationAlerter { } static func isTestFlightBuild() -> Bool { + // If the target environment is a simulator, then + // this is not a TestFlight distribution. Return false. + #if targetEnvironment(simulator) + return false + #endif + // If an "embedded.mobileprovision" is present in the main bundle, then // this is an Xcode, Ad-Hoc, or Enterprise distribution. Return false. if Bundle.main.url(forResource: "embedded", withExtension: "mobileprovision") != nil { From 156401e333ea66f331989f59ed22f0bb189b7ff1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 30 Aug 2023 21:52:31 +0200 Subject: [PATCH 17/49] Refactoring and renaming --- Loop/Views/SettingsView.swift | 73 ++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/Loop/Views/SettingsView.swift b/Loop/Views/SettingsView.swift index a07d623f40..680adcc539 100644 --- a/Loop/Views/SettingsView.swift +++ b/Loop/Views/SettingsView.swift @@ -416,50 +416,53 @@ extension SettingsView { /* DIY loop specific component to show users the amount of time remaining on their build before a rebuild is necessary. */ - private func profileExpirationSection(profileExpiration:Date) -> some View { + private func appExpirationSection(profileExpiration: Date) -> some View { let expirationDate = AppExpirationAlerter.calculateExpirationDate(profileExpiration: profileExpiration) let isTestFlight = AppExpirationAlerter.isTestFlightBuild() - - let nearExpiration : Bool = AppExpirationAlerter.isNearExpiration(expirationDate: expirationDate) + let nearExpiration = AppExpirationAlerter.isNearExpiration(expirationDate: expirationDate) let profileExpirationMsg = AppExpirationAlerter.createProfileExpirationSettingsMessage(expirationDate: expirationDate) let readableExpirationTime = Self.dateFormatter.string(from: expirationDate) if isTestFlight { - return Section(header: SectionHeader(label: NSLocalizedString("TestFlight", comment: "Settings app TestFlight section")), - footer: Text(NSLocalizedString("TestFlight expires ", comment: "Time that build expires") + readableExpirationTime)) { - if(nearExpiration) { - Text(profileExpirationMsg).foregroundColor(.red) - } else { - HStack { - Text("TestFlight Expiration", comment: "Settings TestFlight expiration view") - Spacer() - Text(profileExpirationMsg).foregroundColor(Color.secondary) - } - } - Button(action: { - UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/gh-actions/gh-update/")!) - }) { - Text(NSLocalizedString("How to update (LoopDocs)", comment: "The title text for how to update")) - } - } + return createAppExpirationSection( + headerLabel: NSLocalizedString("TestFlight", comment: "Settings app TestFlight section"), + footerLabel: NSLocalizedString("TestFlight expires ", comment: "Time that build expires") + readableExpirationTime, + expirationLabel: NSLocalizedString("TestFlight Expiration", comment: "Settings TestFlight expiration view"), + updateURL: "https://loopkit.github.io/loopdocs/gh-actions/gh-update/", + nearExpiration: nearExpiration, + expirationMessage: profileExpirationMsg + ) } else { - return Section(header: SectionHeader(label: NSLocalizedString("App Profile", comment: "Settings app profile section")), - footer: Text(NSLocalizedString("Profile expires ", comment: "Time that profile expires") + readableExpirationTime)) { - if(nearExpiration) { - Text(profileExpirationMsg).foregroundColor(.red) - } else { - HStack { - Text("Profile Expiration", comment: "Settings App Profile expiration view") - Spacer() - Text(profileExpirationMsg).foregroundColor(Color.secondary) - } - } - Button(action: { - UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/build/updating/")!) - }) { - Text(NSLocalizedString("How to update (LoopDocs)", comment: "The title text for how to update")) + return createAppExpirationSection( + headerLabel: NSLocalizedString("App Profile", comment: "Settings app profile section"), + footerLabel: NSLocalizedString("Profile expires ", comment: "Time that profile expires") + readableExpirationTime, + expirationLabel: NSLocalizedString("Profile Expiration", comment: "Settings App Profile expiration view"), + updateURL: "https://loopkit.github.io/loopdocs/build/updating/", + nearExpiration: nearExpiration, + expirationMessage: profileExpirationMsg + ) + } + } + + private func createAppExpirationSection(headerLabel: String, footerLabel: String, expirationLabel: String, updateURL: String, nearExpiration: Bool, expirationMessage: String) -> some View { + return Section( + header: SectionHeader(label: headerLabel), + footer: Text(footerLabel) + ) { + if nearExpiration { + Text(expirationMessage).foregroundColor(.red) + } else { + HStack { + Text(expirationLabel) + Spacer() + Text(expirationMessage).foregroundColor(Color.secondary) } } + Button(action: { + UIApplication.shared.open(URL(string: updateURL)!) + }) { + Text(NSLocalizedString("How to update (LoopDocs)", comment: "The title text for how to update")) + } } } From 180eceb4c05274d96f0dadc01dc03abe746b91ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 30 Aug 2023 22:05:00 +0200 Subject: [PATCH 18/49] Refactoring and renaming --- Loop/Views/SettingsView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Loop/Views/SettingsView.swift b/Loop/Views/SettingsView.swift index 680adcc539..958e38f100 100644 --- a/Loop/Views/SettingsView.swift +++ b/Loop/Views/SettingsView.swift @@ -75,7 +75,7 @@ public struct SettingsView: View { supportSection if let profileExpiration = BuildDetails.default.profileExpiration, FeatureFlags.profileExpirationSettingsViewEnabled { - profileExpirationSection(profileExpiration: profileExpiration) + appExpirationSection(profileExpiration: profileExpiration) } } } From a6a057fb85f8ce7e057272faba6daf043790cebd Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Sat, 2 Sep 2023 14:15:36 -0500 Subject: [PATCH 19/49] Tests passing --- Loop.xcodeproj/project.pbxproj | 6 +- Loop/Managers/LoopDataManager.swift | 10 +- .../GlucoseStoreProtocol.swift | 2 +- Loop/Models/LoopConstants.swift | 3 - .../live_capture/live_capture_input.json | 1482 +++++++++++++++++ .../live_capture_predicted_glucose.json | 316 ++-- LoopTests/Managers/LoopAlgorithmTests.swift | 72 +- .../Managers/LoopDataManagerDosingTests.swift | 83 +- LoopTests/Mock Stores/MockDoseStore.swift | 8 +- LoopTests/Mock Stores/MockGlucoseStore.swift | 8 +- 10 files changed, 1775 insertions(+), 215 deletions(-) create mode 100644 LoopTests/Fixtures/live_capture/live_capture_input.json diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 512f50cdf4..78f53ccf42 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -434,6 +434,7 @@ C16B983E26B4893300256B05 /* DoseEnactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C16B983D26B4893300256B05 /* DoseEnactor.swift */; }; C16B984026B4898800256B05 /* DoseEnactorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C16B983F26B4898800256B05 /* DoseEnactorTests.swift */; }; C16DA84222E8E112008624C2 /* PluginManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C16DA84122E8E112008624C2 /* PluginManager.swift */; }; + C16FC0B02A99392F0025E239 /* live_capture_input.json in Resources */ = {isa = PBXBuildFile; fileRef = C16FC0AF2A99392F0025E239 /* live_capture_input.json */; }; C1735B1E2A0809830082BB8A /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = C1735B1D2A0809830082BB8A /* ZIPFoundation */; }; C1742332259BEADC00399C9D /* ManualEntryDoseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1742331259BEADC00399C9D /* ManualEntryDoseView.swift */; }; C174233C259BEB0F00399C9D /* ManualEntryDoseViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C174233B259BEB0F00399C9D /* ManualEntryDoseViewModel.swift */; }; @@ -1446,6 +1447,7 @@ C16B983D26B4893300256B05 /* DoseEnactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DoseEnactor.swift; sourceTree = ""; }; C16B983F26B4898800256B05 /* DoseEnactorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DoseEnactorTests.swift; sourceTree = ""; }; C16DA84122E8E112008624C2 /* PluginManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginManager.swift; sourceTree = ""; }; + C16FC0AF2A99392F0025E239 /* live_capture_input.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = live_capture_input.json; sourceTree = ""; }; C1742331259BEADC00399C9D /* ManualEntryDoseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManualEntryDoseView.swift; sourceTree = ""; }; C174233B259BEB0F00399C9D /* ManualEntryDoseViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManualEntryDoseViewModel.swift; sourceTree = ""; }; C174571329830930009EFCF2 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = ""; }; @@ -1543,8 +1545,8 @@ C1D0B62F2986D4D90098D215 /* LocalizedString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedString.swift; sourceTree = ""; }; C1D197FE232CF92D0096D646 /* capture-build-details.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = "capture-build-details.sh"; sourceTree = ""; }; C1D289B422F90A52003FFBD9 /* BasalDeliveryState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasalDeliveryState.swift; sourceTree = ""; }; - C1D70F7A2A914F71009FE129 /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = he.lproj/InfoPlist.strings; sourceTree = ""; }; C1D476B32A8ED179002C1C87 /* LoopAlgorithmTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopAlgorithmTests.swift; sourceTree = ""; }; + C1D70F7A2A914F71009FE129 /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = he.lproj/InfoPlist.strings; sourceTree = ""; }; C1DA986B2843B6F9001D04CC /* PersistedProperty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistedProperty.swift; sourceTree = ""; }; C1DE5D22251BFC4D00439E49 /* SimpleBolusView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SimpleBolusView.swift; sourceTree = ""; }; C1E2773D224177C000354103 /* ClockKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ClockKit.framework; path = Platforms/WatchOS.platform/Developer/SDKs/WatchOS.sdk/System/Library/Frameworks/ClockKit.framework; sourceTree = DEVELOPER_DIR; }; @@ -2761,6 +2763,7 @@ C13072BD2A76AF97009A7C58 /* live_capture_doses.json */, C13072BF2A76B041009A7C58 /* live_capture_carb_entries.json */, C13072C32A76B0B1009A7C58 /* live_capture_historic_glucose.json */, + C16FC0AF2A99392F0025E239 /* live_capture_input.json */, ); path = live_capture; sourceTree = ""; @@ -3434,6 +3437,7 @@ E90909D124E34AC500F963D2 /* high_and_rising_with_cob_momentum_effect.json in Resources */, E9C58A8024DB529A00487A17 /* insulin_effect.json in Resources */, E90909DF24E34F1600F963D2 /* low_and_falling_insulin_effect.json in Resources */, + C16FC0B02A99392F0025E239 /* live_capture_input.json in Resources */, E93E86BE24E1FDC400FF40C8 /* flat_and_stable_carb_effect.json in Resources */, E90909E824E3530200F963D2 /* low_with_low_treatment_insulin_effect.json in Resources */, E9B3553B293706CB0076AB04 /* noisy_cgm_counteraction_effect.json in Resources */, diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index a97944e9ea..73b7e79abb 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -364,7 +364,7 @@ final class LoopDataManager { private var retrospectiveGlucoseDiscrepancies: [GlucoseEffect]? { didSet { - retrospectiveGlucoseDiscrepanciesSummed = retrospectiveGlucoseDiscrepancies?.combinedSums(of: LoopConstants.retrospectiveCorrectionGroupingInterval * retrospectiveCorrectionGroupingIntervalMultiplier) + retrospectiveGlucoseDiscrepanciesSummed = retrospectiveGlucoseDiscrepancies?.combinedSums(of: LoopMath.retrospectiveCorrectionGroupingInterval * retrospectiveCorrectionGroupingIntervalMultiplier) } } @@ -1005,7 +1005,7 @@ extension LoopDataManager { if glucoseMomentumEffect == nil { updateGroup.enter() - glucoseStore.getRecentMomentumEffect { (result) -> Void in + glucoseStore.getRecentMomentumEffect(for: now()) { (result) -> Void in switch result { case .failure(let error): self.logger.error("Failure getting recent momentum effect: %{public}@", String(describing: error)) @@ -1594,7 +1594,7 @@ extension LoopDataManager { insulinSensitivity: insulinSensitivity, basalRate: basalRate, correctionRange: correctionRange, - retrospectiveCorrectionGroupingInterval: LoopConstants.retrospectiveCorrectionGroupingInterval + retrospectiveCorrectionGroupingInterval: LoopMath.retrospectiveCorrectionGroupingInterval ) } @@ -1605,7 +1605,7 @@ extension LoopDataManager { let correctionRange = settings.glucoseTargetRangeSchedule!.quantityRange(at: glucose.startDate) let retrospectiveGlucoseDiscrepancies = insulinCounteractionEffects.subtracting(carbEffects, withUniformInterval: carbStore.delta) - let retrospectiveGlucoseDiscrepanciesSummed = retrospectiveGlucoseDiscrepancies.combinedSums(of: LoopConstants.retrospectiveCorrectionGroupingInterval * retrospectiveCorrectionGroupingIntervalMultiplier) + let retrospectiveGlucoseDiscrepanciesSummed = retrospectiveGlucoseDiscrepancies.combinedSums(of: LoopMath.retrospectiveCorrectionGroupingInterval * retrospectiveCorrectionGroupingIntervalMultiplier) return retrospectiveCorrection.computeEffect( startingAt: glucose, retrospectiveGlucoseDiscrepanciesSummed: retrospectiveGlucoseDiscrepanciesSummed, @@ -1613,7 +1613,7 @@ extension LoopDataManager { insulinSensitivity: insulinSensitivity, basalRate: basalRate, correctionRange: correctionRange, - retrospectiveCorrectionGroupingInterval: LoopConstants.retrospectiveCorrectionGroupingInterval + retrospectiveCorrectionGroupingInterval: LoopMath.retrospectiveCorrectionGroupingInterval ) } diff --git a/Loop/Managers/Store Protocols/GlucoseStoreProtocol.swift b/Loop/Managers/Store Protocols/GlucoseStoreProtocol.swift index b549d9b0df..adde73c4c7 100644 --- a/Loop/Managers/Store Protocols/GlucoseStoreProtocol.swift +++ b/Loop/Managers/Store Protocols/GlucoseStoreProtocol.swift @@ -29,7 +29,7 @@ protocol GlucoseStoreProtocol: AnyObject { func executeGlucoseQuery(fromQueryAnchor queryAnchor: GlucoseStore.QueryAnchor?, limit: Int, completion: @escaping (GlucoseStore.GlucoseQueryResult) -> Void) // MARK: Effect Calculation - func getRecentMomentumEffect(_ completion: @escaping (_ result: Result<[GlucoseEffect], Error>) -> Void) + func getRecentMomentumEffect(for date: Date?, _ completion: @escaping (_ result: Result<[GlucoseEffect], Error>) -> Void) func getCounteractionEffects(start: Date, end: Date?, to effects: [GlucoseEffect], _ completion: @escaping (_ effects: Result<[GlucoseEffectVelocity], Error>) -> Void) diff --git a/Loop/Models/LoopConstants.swift b/Loop/Models/LoopConstants.swift index a62fc13849..fb69c8275f 100644 --- a/Loop/Models/LoopConstants.swift +++ b/Loop/Models/LoopConstants.swift @@ -52,9 +52,6 @@ enum LoopConstants { // Percentage of recommended dose to apply as bolus when using automatic bolus dosing strategy static let bolusPartialApplicationFactor = 0.4 - /// The interval over which to aggregate changes in glucose for retrospective correction - static let retrospectiveCorrectionGroupingInterval = TimeInterval(minutes: 30) - /// Loop completion aging category limits static let completionFreshLimit = TimeInterval(minutes: 6) static let completionAgingLimit = TimeInterval(minutes: 16) diff --git a/LoopTests/Fixtures/live_capture/live_capture_input.json b/LoopTests/Fixtures/live_capture/live_capture_input.json new file mode 100644 index 0000000000..95c27a166b --- /dev/null +++ b/LoopTests/Fixtures/live_capture/live_capture_input.json @@ -0,0 +1,1482 @@ +{ + "carbEntries" : [ + { + "absorptionTime" : 10800, + "quantity" : 20, + "startDate" : "2023-08-17T20:05:47Z" + }, + { + "absorptionTime" : 10800, + "quantity" : 10, + "startDate" : "2023-08-18T18:01:14Z" + } + ], + "doses" : [ + { + "endDate" : "2023-08-17T18:21:55Z", + "startDate" : "2023-08-17T18:01:54Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-08-17T18:42:08Z", + "startDate" : "2023-08-17T18:21:55Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-08-17T18:47:09Z", + "startDate" : "2023-08-17T18:42:08Z", + "type" : "basal", + "unit" : "U", + "value" : 0.1 + }, + { + "endDate" : "2023-08-17T19:07:04Z", + "startDate" : "2023-08-17T18:47:09Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-08-17T19:11:53Z", + "startDate" : "2023-08-17T19:07:04Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-08-17T19:26:55Z", + "startDate" : "2023-08-17T19:11:53Z", + "type" : "basal", + "unit" : "U", + "value" : 0.3 + }, + { + "endDate" : "2023-08-17T19:47:02Z", + "startDate" : "2023-08-17T19:26:55Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-08-17T20:06:54Z", + "startDate" : "2023-08-17T19:47:02Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-08-17T20:26:51Z", + "startDate" : "2023-08-17T20:06:54Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-08-17T20:46:53Z", + "startDate" : "2023-08-17T20:26:51Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-08-17T21:06:52Z", + "startDate" : "2023-08-17T20:46:53Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-08-17T21:46:54Z", + "startDate" : "2023-08-17T21:06:52Z", + "type" : "basal", + "unit" : "U", + "value" : 0.75 + }, + { + "endDate" : "2023-08-17T21:51:52Z", + "startDate" : "2023-08-17T21:46:54Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-08-17T21:56:54Z", + "startDate" : "2023-08-17T21:51:52Z", + "type" : "basal", + "unit" : "U", + "value" : 0.1 + }, + { + "endDate" : "2023-08-17T22:11:51Z", + "startDate" : "2023-08-17T21:56:54Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-08-17T23:07:00Z", + "startDate" : "2023-08-17T22:11:51Z", + "type" : "basal", + "unit" : "U", + "value" : 1 + }, + { + "endDate" : "2023-08-17T23:11:53Z", + "startDate" : "2023-08-17T23:07:00Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-08-17T23:42:02Z", + "startDate" : "2023-08-17T23:11:53Z", + "type" : "basal", + "unit" : "U", + "value" : 0.55 + }, + { + "endDate" : "2023-08-18T00:02:07Z", + "startDate" : "2023-08-17T23:42:02Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-08-18T00:16:52Z", + "startDate" : "2023-08-18T00:02:07Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-08-18T01:26:52Z", + "startDate" : "2023-08-18T00:16:52Z", + "type" : "basal", + "unit" : "U", + "value" : 1.3 + }, + { + "endDate" : "2023-08-18T01:46:54Z", + "startDate" : "2023-08-18T01:26:52Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-08-18T01:56:52Z", + "startDate" : "2023-08-18T01:46:54Z", + "type" : "basal", + "unit" : "U", + "value" : 0.2 + }, + { + "endDate" : "2023-08-18T02:11:53Z", + "startDate" : "2023-08-18T01:56:52Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-08-18T02:41:53Z", + "startDate" : "2023-08-18T02:11:53Z", + "type" : "basal", + "unit" : "U", + "value" : 0.55 + }, + { + "endDate" : "2023-08-18T02:47:02Z", + "startDate" : "2023-08-18T02:41:53Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-08-18T03:01:55Z", + "startDate" : "2023-08-18T02:47:02Z", + "type" : "basal", + "unit" : "U", + "value" : 0.25 + }, + { + "endDate" : "2023-08-18T03:06:52Z", + "startDate" : "2023-08-18T03:01:55Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-08-18T04:06:54Z", + "startDate" : "2023-08-18T03:06:52Z", + "type" : "basal", + "unit" : "U", + "value" : 1.1 + }, + { + "endDate" : "2023-08-18T03:22:02Z", + "startDate" : "2023-08-18T03:21:50Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.3 + }, + { + "endDate" : "2023-08-18T03:27:09Z", + "startDate" : "2023-08-18T03:27:05Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.1 + }, + { + "endDate" : "2023-08-18T03:31:54Z", + "startDate" : "2023-08-18T03:31:50Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.1 + }, + { + "endDate" : "2023-08-18T03:41:53Z", + "startDate" : "2023-08-18T03:41:49Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.1 + }, + { + "endDate" : "2023-08-18T04:11:52Z", + "startDate" : "2023-08-18T04:06:54Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0.75 + }, + { + "endDate" : "2023-08-18T04:21:54Z", + "startDate" : "2023-08-18T04:11:52Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0.65 + }, + { + "endDate" : "2023-08-18T04:26:54Z", + "startDate" : "2023-08-18T04:21:54Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0.45 + }, + { + "endDate" : "2023-08-18T04:41:56Z", + "startDate" : "2023-08-18T04:26:54Z", + "type" : "basal", + "unit" : "U", + "value" : 0.3 + }, + { + "endDate" : "2023-08-18T04:46:57Z", + "startDate" : "2023-08-18T04:41:56Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0.4 + }, + { + "endDate" : "2023-08-18T04:51:52Z", + "startDate" : "2023-08-18T04:46:57Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0.1 + }, + { + "endDate" : "2023-08-18T05:00:00Z", + "startDate" : "2023-08-18T04:51:52Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-08-18T05:01:54Z", + "startDate" : "2023-08-18T05:00:00Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-08-18T05:11:52Z", + "startDate" : "2023-08-18T05:01:54Z", + "type" : "basal", + "unit" : "U", + "value" : 0.2 + }, + { + "endDate" : "2023-08-18T05:16:57Z", + "startDate" : "2023-08-18T05:11:52Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-08-18T05:27:01Z", + "startDate" : "2023-08-18T05:16:57Z", + "type" : "basal", + "unit" : "U", + "value" : 0.2 + }, + { + "endDate" : "2023-08-18T05:46:52Z", + "startDate" : "2023-08-18T05:27:01Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-08-18T06:01:53Z", + "startDate" : "2023-08-18T05:46:52Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-08-18T06:06:56Z", + "startDate" : "2023-08-18T06:01:53Z", + "type" : "basal", + "unit" : "U", + "value" : 0.1 + }, + { + "endDate" : "2023-08-18T06:22:05Z", + "startDate" : "2023-08-18T06:06:56Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-08-18T07:16:55Z", + "startDate" : "2023-08-18T06:22:05Z", + "type" : "basal", + "unit" : "U", + "value" : 1 + }, + { + "endDate" : "2023-08-18T07:26:58Z", + "startDate" : "2023-08-18T07:16:55Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-08-18T08:36:59Z", + "startDate" : "2023-08-18T07:26:58Z", + "type" : "basal", + "unit" : "U", + "value" : 1.3 + }, + { + "endDate" : "2023-08-18T08:41:52Z", + "startDate" : "2023-08-18T08:36:59Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-08-18T08:51:52Z", + "startDate" : "2023-08-18T08:41:52Z", + "type" : "basal", + "unit" : "U", + "value" : 0.2 + }, + { + "endDate" : "2023-08-18T08:56:58Z", + "startDate" : "2023-08-18T08:51:52Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-08-18T09:12:01Z", + "startDate" : "2023-08-18T08:56:58Z", + "type" : "basal", + "unit" : "U", + "value" : 0.3 + }, + { + "endDate" : "2023-08-18T09:42:01Z", + "startDate" : "2023-08-18T09:12:01Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-08-18T09:51:54Z", + "startDate" : "2023-08-18T09:42:01Z", + "type" : "basal", + "unit" : "U", + "value" : 0.2 + }, + { + "endDate" : "2023-08-18T09:56:52Z", + "startDate" : "2023-08-18T09:51:54Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-08-18T10:06:56Z", + "startDate" : "2023-08-18T09:56:52Z", + "type" : "basal", + "unit" : "U", + "value" : 0.2 + }, + { + "endDate" : "2023-08-18T10:11:54Z", + "startDate" : "2023-08-18T10:06:56Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-08-18T10:37:02Z", + "startDate" : "2023-08-18T10:11:54Z", + "type" : "basal", + "unit" : "U", + "value" : 0.45 + }, + { + "endDate" : "2023-08-18T10:41:54Z", + "startDate" : "2023-08-18T10:37:02Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-08-18T11:36:58Z", + "startDate" : "2023-08-18T10:41:54Z", + "type" : "basal", + "unit" : "U", + "value" : 1 + }, + { + "endDate" : "2023-08-18T11:51:54Z", + "startDate" : "2023-08-18T11:36:58Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-08-18T12:41:54Z", + "startDate" : "2023-08-18T11:51:54Z", + "type" : "basal", + "unit" : "U", + "value" : 0.9 + }, + { + "endDate" : "2023-08-18T12:57:00Z", + "startDate" : "2023-08-18T12:41:54Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-08-18T14:11:45Z", + "startDate" : "2023-08-18T12:57:00Z", + "type" : "basal", + "unit" : "U", + "value" : 1.35 + }, + { + "endDate" : "2023-08-18T14:17:00Z", + "startDate" : "2023-08-18T14:11:45Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-08-18T14:56:55Z", + "startDate" : "2023-08-18T14:17:00Z", + "type" : "basal", + "unit" : "U", + "value" : 0.75 + }, + { + "endDate" : "2023-08-18T15:17:08Z", + "startDate" : "2023-08-18T14:56:55Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-08-18T17:22:08Z", + "startDate" : "2023-08-18T15:17:08Z", + "type" : "basal", + "unit" : "U", + "value" : 2.3 + }, + { + "endDate" : "2023-08-18T16:12:07Z", + "startDate" : "2023-08-18T16:12:03Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.1 + }, + { + "endDate" : "2023-08-18T16:37:09Z", + "startDate" : "2023-08-18T16:37:05Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.1 + }, + { + "endDate" : "2023-08-18T16:42:09Z", + "startDate" : "2023-08-18T16:42:05Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.1 + }, + { + "endDate" : "2023-08-18T16:47:14Z", + "startDate" : "2023-08-18T16:47:10Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.1 + }, + { + "endDate" : "2023-08-18T17:22:10Z", + "startDate" : "2023-08-18T17:22:08Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0.7 + }, + { + "endDate" : "2023-08-18T17:27:08Z", + "startDate" : "2023-08-18T17:22:10Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0.75 + }, + { + "endDate" : "2023-08-18T17:57:10Z", + "startDate" : "2023-08-18T17:27:08Z", + "type" : "basal", + "unit" : "U", + "value" : 0.55 + }, + { + "endDate" : "2023-08-18T18:02:05Z", + "startDate" : "2023-08-18T17:57:10Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0.55 + }, + { + "endDate" : "2023-08-18T18:01:46Z", + "startDate" : "2023-08-18T18:01:30Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.4 + } + ], + "glucoseHistory" : [ + { + "quantity" : 81, + "startDate" : "2023-08-18T00:16:23Z" + }, + { + "quantity" : 82, + "startDate" : "2023-08-18T00:21:25Z" + }, + { + "quantity" : 84, + "startDate" : "2023-08-18T00:26:30Z" + }, + { + "quantity" : 85, + "startDate" : "2023-08-18T00:31:24Z" + }, + { + "quantity" : 88, + "startDate" : "2023-08-18T00:36:23Z" + }, + { + "quantity" : 89, + "startDate" : "2023-08-18T00:41:22Z" + }, + { + "quantity" : 90, + "startDate" : "2023-08-18T00:46:22Z" + }, + { + "quantity" : 90, + "startDate" : "2023-08-18T00:51:23Z" + }, + { + "quantity" : 91, + "startDate" : "2023-08-18T00:56:23Z" + }, + { + "quantity" : 91, + "startDate" : "2023-08-18T01:01:24Z" + }, + { + "quantity" : 89, + "startDate" : "2023-08-18T01:06:25Z" + }, + { + "quantity" : 87, + "startDate" : "2023-08-18T01:11:22Z" + }, + { + "quantity" : 87, + "startDate" : "2023-08-18T01:16:23Z" + }, + { + "quantity" : 85, + "startDate" : "2023-08-18T01:21:23Z" + }, + { + "quantity" : 78, + "startDate" : "2023-08-18T01:26:22Z" + }, + { + "quantity" : 80, + "startDate" : "2023-08-18T01:31:24Z" + }, + { + "quantity" : 79, + "startDate" : "2023-08-18T01:36:25Z" + }, + { + "quantity" : 81, + "startDate" : "2023-08-18T01:41:22Z" + }, + { + "quantity" : 82, + "startDate" : "2023-08-18T01:46:23Z" + }, + { + "quantity" : 82, + "startDate" : "2023-08-18T01:51:23Z" + }, + { + "quantity" : 81, + "startDate" : "2023-08-18T01:56:23Z" + }, + { + "quantity" : 80, + "startDate" : "2023-08-18T02:01:25Z" + }, + { + "quantity" : 79, + "startDate" : "2023-08-18T02:06:29Z" + }, + { + "quantity" : 81, + "startDate" : "2023-08-18T02:11:23Z" + }, + { + "quantity" : 84, + "startDate" : "2023-08-18T02:16:25Z" + }, + { + "quantity" : 86, + "startDate" : "2023-08-18T02:21:27Z" + }, + { + "quantity" : 87, + "startDate" : "2023-08-18T02:26:24Z" + }, + { + "quantity" : 84, + "startDate" : "2023-08-18T02:31:26Z" + }, + { + "quantity" : 84, + "startDate" : "2023-08-18T02:36:25Z" + }, + { + "quantity" : 82, + "startDate" : "2023-08-18T02:41:23Z" + }, + { + "quantity" : 82, + "startDate" : "2023-08-18T02:46:31Z" + }, + { + "quantity" : 99, + "startDate" : "2023-08-18T02:51:29Z" + }, + { + "quantity" : 83, + "startDate" : "2023-08-18T02:56:25Z" + }, + { + "quantity" : 83, + "startDate" : "2023-08-18T03:01:27Z" + }, + { + "quantity" : 87, + "startDate" : "2023-08-18T03:06:23Z" + }, + { + "quantity" : 90, + "startDate" : "2023-08-18T03:11:25Z" + }, + { + "quantity" : 94, + "startDate" : "2023-08-18T03:16:29Z" + }, + { + "quantity" : 101, + "startDate" : "2023-08-18T03:21:22Z" + }, + { + "quantity" : 103, + "startDate" : "2023-08-18T03:26:39Z" + }, + { + "quantity" : 104, + "startDate" : "2023-08-18T03:31:22Z" + }, + { + "quantity" : 106, + "startDate" : "2023-08-18T03:36:22Z" + }, + { + "quantity" : 108, + "startDate" : "2023-08-18T03:41:22Z" + }, + { + "quantity" : 108, + "startDate" : "2023-08-18T03:46:25Z" + }, + { + "quantity" : 109, + "startDate" : "2023-08-18T03:51:24Z" + }, + { + "quantity" : 110, + "startDate" : "2023-08-18T03:56:25Z" + }, + { + "quantity" : 110, + "startDate" : "2023-08-18T04:01:27Z" + }, + { + "quantity" : 109, + "startDate" : "2023-08-18T04:06:24Z" + }, + { + "quantity" : 108, + "startDate" : "2023-08-18T04:11:23Z" + }, + { + "quantity" : 106, + "startDate" : "2023-08-18T04:16:23Z" + }, + { + "quantity" : 105, + "startDate" : "2023-08-18T04:21:24Z" + }, + { + "quantity" : 105, + "startDate" : "2023-08-18T04:26:23Z" + }, + { + "quantity" : 106, + "startDate" : "2023-08-18T04:31:26Z" + }, + { + "quantity" : 106, + "startDate" : "2023-08-18T04:36:24Z" + }, + { + "quantity" : 102, + "startDate" : "2023-08-18T04:41:25Z" + }, + { + "quantity" : 99, + "startDate" : "2023-08-18T04:46:27Z" + }, + { + "quantity" : 99, + "startDate" : "2023-08-18T04:51:22Z" + }, + { + "quantity" : 97, + "startDate" : "2023-08-18T04:56:24Z" + }, + { + "quantity" : 96, + "startDate" : "2023-08-18T05:01:23Z" + }, + { + "quantity" : 97, + "startDate" : "2023-08-18T05:06:24Z" + }, + { + "quantity" : 95, + "startDate" : "2023-08-18T05:11:22Z" + }, + { + "quantity" : 93, + "startDate" : "2023-08-18T05:16:26Z" + }, + { + "quantity" : 92, + "startDate" : "2023-08-18T05:21:22Z" + }, + { + "quantity" : 90, + "startDate" : "2023-08-18T05:26:33Z" + }, + { + "quantity" : 87, + "startDate" : "2023-08-18T05:31:22Z" + }, + { + "quantity" : 85, + "startDate" : "2023-08-18T05:36:23Z" + }, + { + "quantity" : 82, + "startDate" : "2023-08-18T05:41:36Z" + }, + { + "quantity" : 83, + "startDate" : "2023-08-18T05:46:22Z" + }, + { + "quantity" : 78, + "startDate" : "2023-08-18T05:51:23Z" + }, + { + "quantity" : 79, + "startDate" : "2023-08-18T05:56:25Z" + }, + { + "quantity" : 80, + "startDate" : "2023-08-18T06:01:24Z" + }, + { + "quantity" : 79, + "startDate" : "2023-08-18T06:06:26Z" + }, + { + "quantity" : 78, + "startDate" : "2023-08-18T06:11:29Z" + }, + { + "quantity" : 79, + "startDate" : "2023-08-18T06:16:24Z" + }, + { + "quantity" : 81, + "startDate" : "2023-08-18T06:21:35Z" + }, + { + "quantity" : 82, + "startDate" : "2023-08-18T06:26:31Z" + }, + { + "quantity" : 86, + "startDate" : "2023-08-18T06:31:25Z" + }, + { + "quantity" : 84, + "startDate" : "2023-08-18T06:36:30Z" + }, + { + "quantity" : 83, + "startDate" : "2023-08-18T06:41:22Z" + }, + { + "quantity" : 83, + "startDate" : "2023-08-18T06:46:22Z" + }, + { + "quantity" : 84, + "startDate" : "2023-08-18T06:51:31Z" + }, + { + "quantity" : 85, + "startDate" : "2023-08-18T06:56:35Z" + }, + { + "quantity" : 85, + "startDate" : "2023-08-18T07:01:25Z" + }, + { + "quantity" : 84, + "startDate" : "2023-08-18T07:06:29Z" + }, + { + "quantity" : 83, + "startDate" : "2023-08-18T07:11:22Z" + }, + { + "quantity" : 82, + "startDate" : "2023-08-18T07:16:24Z" + }, + { + "quantity" : 81, + "startDate" : "2023-08-18T07:21:31Z" + }, + { + "quantity" : 83, + "startDate" : "2023-08-18T07:26:27Z" + }, + { + "quantity" : 83, + "startDate" : "2023-08-18T07:31:22Z" + }, + { + "quantity" : 84, + "startDate" : "2023-08-18T07:36:24Z" + }, + { + "quantity" : 84, + "startDate" : "2023-08-18T07:41:23Z" + }, + { + "quantity" : 85, + "startDate" : "2023-08-18T07:46:23Z" + }, + { + "quantity" : 86, + "startDate" : "2023-08-18T07:51:25Z" + }, + { + "quantity" : 89, + "startDate" : "2023-08-18T07:56:24Z" + }, + { + "quantity" : 90, + "startDate" : "2023-08-18T08:01:26Z" + }, + { + "quantity" : 90, + "startDate" : "2023-08-18T08:06:33Z" + }, + { + "quantity" : 91, + "startDate" : "2023-08-18T08:11:22Z" + }, + { + "quantity" : 90, + "startDate" : "2023-08-18T08:16:23Z" + }, + { + "quantity" : 89, + "startDate" : "2023-08-18T08:21:26Z" + }, + { + "quantity" : 89, + "startDate" : "2023-08-18T08:26:29Z" + }, + { + "quantity" : 88, + "startDate" : "2023-08-18T08:31:25Z" + }, + { + "quantity" : 87, + "startDate" : "2023-08-18T08:36:30Z" + }, + { + "quantity" : 87, + "startDate" : "2023-08-18T08:41:22Z" + }, + { + "quantity" : 87, + "startDate" : "2023-08-18T08:46:28Z" + }, + { + "quantity" : 86, + "startDate" : "2023-08-18T08:51:23Z" + }, + { + "quantity" : 86, + "startDate" : "2023-08-18T08:56:28Z" + }, + { + "quantity" : 86, + "startDate" : "2023-08-18T09:01:26Z" + }, + { + "quantity" : 86, + "startDate" : "2023-08-18T09:06:23Z" + }, + { + "quantity" : 85, + "startDate" : "2023-08-18T09:11:30Z" + }, + { + "quantity" : 85, + "startDate" : "2023-08-18T09:16:25Z" + }, + { + "quantity" : 84, + "startDate" : "2023-08-18T09:21:25Z" + }, + { + "quantity" : 84, + "startDate" : "2023-08-18T09:26:25Z" + }, + { + "quantity" : 83, + "startDate" : "2023-08-18T09:31:25Z" + }, + { + "quantity" : 83, + "startDate" : "2023-08-18T09:36:25Z" + }, + { + "quantity" : 82, + "startDate" : "2023-08-18T09:41:25Z" + }, + { + "quantity" : 82, + "startDate" : "2023-08-18T09:46:25Z" + }, + { + "quantity" : 81, + "startDate" : "2023-08-18T09:51:25Z" + }, + { + "quantity" : 82, + "startDate" : "2023-08-18T09:56:22Z" + }, + { + "quantity" : 81, + "startDate" : "2023-08-18T10:01:27Z" + }, + { + "quantity" : 81, + "startDate" : "2023-08-18T10:06:27Z" + }, + { + "quantity" : 81, + "startDate" : "2023-08-18T10:11:25Z" + }, + { + "quantity" : 82, + "startDate" : "2023-08-18T10:16:23Z" + }, + { + "quantity" : 83, + "startDate" : "2023-08-18T10:21:24Z" + }, + { + "quantity" : 83, + "startDate" : "2023-08-18T10:26:23Z" + }, + { + "quantity" : 82, + "startDate" : "2023-08-18T10:31:34Z" + }, + { + "quantity" : 81, + "startDate" : "2023-08-18T10:36:31Z" + }, + { + "quantity" : 81, + "startDate" : "2023-08-18T10:41:24Z" + }, + { + "quantity" : 82, + "startDate" : "2023-08-18T10:46:28Z" + }, + { + "quantity" : 81, + "startDate" : "2023-08-18T10:51:23Z" + }, + { + "quantity" : 82, + "startDate" : "2023-08-18T10:56:26Z" + }, + { + "quantity" : 84, + "startDate" : "2023-08-18T11:01:23Z" + }, + { + "quantity" : 84, + "startDate" : "2023-08-18T11:06:26Z" + }, + { + "quantity" : 85, + "startDate" : "2023-08-18T11:11:26Z" + }, + { + "quantity" : 87, + "startDate" : "2023-08-18T11:16:24Z" + }, + { + "quantity" : 88, + "startDate" : "2023-08-18T11:21:24Z" + }, + { + "quantity" : 88, + "startDate" : "2023-08-18T11:26:27Z" + }, + { + "quantity" : 84, + "startDate" : "2023-08-18T11:31:23Z" + }, + { + "quantity" : 82, + "startDate" : "2023-08-18T11:36:27Z" + }, + { + "quantity" : 81, + "startDate" : "2023-08-18T11:41:26Z" + }, + { + "quantity" : 81, + "startDate" : "2023-08-18T11:46:23Z" + }, + { + "quantity" : 84, + "startDate" : "2023-08-18T11:51:24Z" + }, + { + "quantity" : 84, + "startDate" : "2023-08-18T11:56:23Z" + }, + { + "quantity" : 86, + "startDate" : "2023-08-18T12:01:24Z" + }, + { + "quantity" : 86, + "startDate" : "2023-08-18T12:06:25Z" + }, + { + "quantity" : 86, + "startDate" : "2023-08-18T12:11:24Z" + }, + { + "quantity" : 86, + "startDate" : "2023-08-18T12:16:24Z" + }, + { + "quantity" : 86, + "startDate" : "2023-08-18T12:21:25Z" + }, + { + "quantity" : 87, + "startDate" : "2023-08-18T12:26:23Z" + }, + { + "quantity" : 87, + "startDate" : "2023-08-18T12:31:25Z" + }, + { + "quantity" : 88, + "startDate" : "2023-08-18T12:36:32Z" + }, + { + "quantity" : 81, + "startDate" : "2023-08-18T12:41:23Z" + }, + { + "quantity" : 78, + "startDate" : "2023-08-18T12:46:23Z" + }, + { + "quantity" : 77, + "startDate" : "2023-08-18T12:51:23Z" + }, + { + "quantity" : 84, + "startDate" : "2023-08-18T12:56:28Z" + }, + { + "quantity" : 88, + "startDate" : "2023-08-18T13:01:24Z" + }, + { + "quantity" : 90, + "startDate" : "2023-08-18T13:06:23Z" + }, + { + "quantity" : 85, + "startDate" : "2023-08-18T13:11:23Z" + }, + { + "quantity" : 86, + "startDate" : "2023-08-18T13:16:23Z" + }, + { + "quantity" : 89, + "startDate" : "2023-08-18T13:21:24Z" + }, + { + "quantity" : 86, + "startDate" : "2023-08-18T13:26:28Z" + }, + { + "quantity" : 86, + "startDate" : "2023-08-18T13:31:24Z" + }, + { + "quantity" : 87, + "startDate" : "2023-08-18T13:36:29Z" + }, + { + "quantity" : 90, + "startDate" : "2023-08-18T13:41:23Z" + }, + { + "quantity" : 89, + "startDate" : "2023-08-18T13:46:31Z" + }, + { + "quantity" : 88, + "startDate" : "2023-08-18T13:51:29Z" + }, + { + "quantity" : 92, + "startDate" : "2023-08-18T13:56:33Z" + }, + { + "quantity" : 95, + "startDate" : "2023-08-18T14:01:37Z" + }, + { + "quantity" : 88, + "startDate" : "2023-08-18T14:06:23Z" + }, + { + "quantity" : 86, + "startDate" : "2023-08-18T14:11:26Z" + }, + { + "quantity" : 92, + "startDate" : "2023-08-18T14:16:30Z" + }, + { + "quantity" : 94, + "startDate" : "2023-08-18T14:21:29Z" + }, + { + "quantity" : 96, + "startDate" : "2023-08-18T14:26:23Z" + }, + { + "quantity" : 96, + "startDate" : "2023-08-18T14:31:23Z" + }, + { + "quantity" : 98, + "startDate" : "2023-08-18T14:36:32Z" + }, + { + "quantity" : 98, + "startDate" : "2023-08-18T14:41:25Z" + }, + { + "quantity" : 99, + "startDate" : "2023-08-18T14:46:24Z" + }, + { + "quantity" : 98, + "startDate" : "2023-08-18T14:51:26Z" + }, + { + "quantity" : 94, + "startDate" : "2023-08-18T14:56:25Z" + }, + { + "quantity" : 87, + "startDate" : "2023-08-18T15:01:24Z" + }, + { + "quantity" : 85, + "startDate" : "2023-08-18T15:06:24Z" + }, + { + "quantity" : 85, + "startDate" : "2023-08-18T15:11:23Z" + }, + { + "quantity" : 86, + "startDate" : "2023-08-18T15:16:24Z" + }, + { + "quantity" : 88, + "startDate" : "2023-08-18T15:21:23Z" + }, + { + "quantity" : 89, + "startDate" : "2023-08-18T15:26:26Z" + }, + { + "quantity" : 91, + "startDate" : "2023-08-18T15:31:24Z" + }, + { + "quantity" : 92, + "startDate" : "2023-08-18T15:36:24Z" + }, + { + "quantity" : 94, + "startDate" : "2023-08-18T15:41:25Z" + }, + { + "quantity" : 96, + "startDate" : "2023-08-18T15:46:25Z" + }, + { + "quantity" : 96, + "startDate" : "2023-08-18T15:51:28Z" + }, + { + "quantity" : 98, + "startDate" : "2023-08-18T15:56:26Z" + }, + { + "quantity" : 97, + "startDate" : "2023-08-18T16:01:23Z" + }, + { + "quantity" : 99, + "startDate" : "2023-08-18T16:06:23Z" + }, + { + "quantity" : 101, + "startDate" : "2023-08-18T16:11:25Z" + }, + { + "quantity" : 102, + "startDate" : "2023-08-18T16:16:25Z" + }, + { + "quantity" : 103, + "startDate" : "2023-08-18T16:21:23Z" + }, + { + "quantity" : 98, + "startDate" : "2023-08-18T16:26:23Z" + }, + { + "quantity" : 102, + "startDate" : "2023-08-18T16:31:28Z" + }, + { + "quantity" : 105, + "startDate" : "2023-08-18T16:36:23Z" + }, + { + "quantity" : 112, + "startDate" : "2023-08-18T16:41:23Z" + }, + { + "quantity" : 112, + "startDate" : "2023-08-18T16:46:23Z" + }, + { + "quantity" : 111, + "startDate" : "2023-08-18T16:51:26Z" + }, + { + "quantity" : 111, + "startDate" : "2023-08-18T16:56:24Z" + }, + { + "quantity" : 111, + "startDate" : "2023-08-18T17:01:23Z" + }, + { + "quantity" : 109, + "startDate" : "2023-08-18T17:06:24Z" + }, + { + "quantity" : 111, + "startDate" : "2023-08-18T17:11:23Z" + }, + { + "quantity" : 111, + "startDate" : "2023-08-18T17:16:23Z" + }, + { + "quantity" : 110, + "startDate" : "2023-08-18T17:21:23Z" + }, + { + "quantity" : 111, + "startDate" : "2023-08-18T17:26:23Z" + }, + { + "quantity" : 110, + "startDate" : "2023-08-18T17:31:25Z" + }, + { + "quantity" : 110, + "startDate" : "2023-08-18T17:36:23Z" + }, + { + "quantity" : 110, + "startDate" : "2023-08-18T17:41:26Z" + }, + { + "quantity" : 110, + "startDate" : "2023-08-18T17:46:30Z" + }, + { + "quantity" : 111, + "startDate" : "2023-08-18T17:51:28Z" + }, + { + "quantity" : 106, + "startDate" : "2023-08-18T17:56:24Z" + }, + { + "quantity" : 102, + "startDate" : "2023-08-18T18:01:24Z" + } + ], + "settings" : { + "basal" : [ + { + "endDate" : "2023-08-18T05:00:00Z", + "startDate" : "2023-08-17T05:00:00Z", + "value" : 1.1 + }, + { + "endDate" : "2023-08-19T05:00:00Z", + "startDate" : "2023-08-18T05:00:00Z", + "value" : 1.1 + } + ], + "carbRatio" : [ + { + "endDate" : "2023-08-18T05:00:00Z", + "startDate" : "2023-08-17T05:00:00Z", + "value" : 15 + }, + { + "endDate" : "2023-08-19T05:00:00Z", + "startDate" : "2023-08-18T05:00:00Z", + "value" : 15 + } + ], + "sensitivity" : [ + { + "endDate" : "2023-08-18T05:00:00Z", + "startDate" : "2023-08-17T05:00:00Z", + "value" : 50 + }, + { + "endDate" : "2023-08-19T05:00:00Z", + "startDate" : "2023-08-18T05:00:00Z", + "value" : 50 + } + ], + "target" : [ + { + "endDate" : "2023-08-19T00:15:00Z", + "startDate" : "2023-08-18T11:50:00Z", + "value" : { + "maxValue" : 115, + "minValue" : 100 + } + } + ] + } +} diff --git a/LoopTests/Fixtures/live_capture/live_capture_predicted_glucose.json b/LoopTests/Fixtures/live_capture/live_capture_predicted_glucose.json index 8ec8f4688c..bb3a141d73 100644 --- a/LoopTests/Fixtures/live_capture/live_capture_predicted_glucose.json +++ b/LoopTests/Fixtures/live_capture/live_capture_predicted_glucose.json @@ -1,322 +1,382 @@ [ { + "quantity" : 102, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T19:20:54Z", - "quantity" : 170 + "startDate" : "2023-08-18T18:01:24Z" }, { + "quantity" : 99.896393318404151, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T19:25:00Z", - "quantity" : 174.50999999999999 + "startDate" : "2023-08-18T18:05:00Z" }, { + "quantity" : 97.327016523041976, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T19:30:00Z", - "quantity" : 177.56455527893289 + "startDate" : "2023-08-18T18:10:00Z" }, { + "quantity" : 95.324444788485806, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T19:35:00Z", - "quantity" : 177.67338252385397 + "startDate" : "2023-08-18T18:15:00Z" }, { + "quantity" : 93.992918445816656, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T19:40:00Z", - "quantity" : 174.91272423228034 + "startDate" : "2023-08-18T18:20:00Z" }, { + "quantity" : 92.98678930503138, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T19:45:00Z", - "quantity" : 171.75695070238604 + "startDate" : "2023-08-18T18:25:00Z" }, { + "quantity" : 92.153401829419991, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T19:50:00Z", - "quantity" : 168.76244616110549 + "startDate" : "2023-08-18T18:30:00Z" }, { + "quantity" : 91.502805972419452, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T19:55:00Z", - "quantity" : 165.95427643567874 + "startDate" : "2023-08-18T18:35:00Z" }, { + "quantity" : 91.044140230125493, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T20:00:00Z", - "quantity" : 163.35383111783804 + "startDate" : "2023-08-18T18:40:00Z" }, { + "quantity" : 90.785701374247225, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T20:05:00Z", - "quantity" : 160.97916167823055 + "startDate" : "2023-08-18T18:45:00Z" }, { + "quantity" : 90.735009333107385, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T20:10:00Z", - "quantity" : 158.84529353872236 + "startDate" : "2023-08-18T18:50:00Z" }, { + "quantity" : 90.87529571413188, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T20:15:00Z", - "quantity" : 156.96451392177406 + "startDate" : "2023-08-18T18:55:00Z" }, { + "quantity" : 91.131206561299791, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T20:20:00Z", - "quantity" : 155.34663717673348 + "startDate" : "2023-08-18T19:00:00Z" }, { + "quantity" : 91.369852449274902, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T20:25:00Z", - "quantity" : 153.99924917105037 + "startDate" : "2023-08-18T19:05:00Z" }, { + "quantity" : 91.596637899447671, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T20:30:00Z", - "quantity" : 152.92793222961055 + "startDate" : "2023-08-18T19:10:00Z" }, { + "quantity" : 91.815952582397927, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T20:35:00Z", - "quantity" : 152.13647200718029 + "startDate" : "2023-08-18T19:15:00Z" }, { + "quantity" : 92.031661009999482, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T20:40:00Z", - "quantity" : 151.62704758695727 + "startDate" : "2023-08-18T19:20:00Z" }, { + "quantity" : 92.24719829385748, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T20:45:00Z", - "quantity" : 151.36168828514261 + "startDate" : "2023-08-18T19:25:00Z" }, { + "quantity" : 92.465606922060431, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T20:50:00Z", - "quantity" : 151.16659478660213 + "startDate" : "2023-08-18T19:30:00Z" }, { + "quantity" : 92.689569938551358, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T20:55:00Z", - "quantity" : 151.01925161349905 + "startDate" : "2023-08-18T19:35:00Z" }, { + "quantity" : 92.921441679246016, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T21:00:00Z", - "quantity" : 150.9170176497727 + "startDate" : "2023-08-18T19:40:00Z" }, { + "quantity" : 93.163276230779218, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T21:05:00Z", - "quantity" : 150.85638361169495 + "startDate" : "2023-08-18T19:45:00Z" }, { + "quantity" : 93.416853767095574, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T21:10:00Z", - "quantity" : 150.83308419477353 + "startDate" : "2023-08-18T19:50:00Z" }, { + "quantity" : 93.683704909090238, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T21:15:00Z", - "quantity" : 150.84043373508726 + "startDate" : "2023-08-18T19:55:00Z" }, { + "quantity" : 93.965133243123091, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T21:20:00Z", - "quantity" : 150.86809756991411 + "startDate" : "2023-08-18T20:00:00Z" }, { + "quantity" : 94.262236125417473, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T21:25:00Z", - "quantity" : 150.9067680932084 + "startDate" : "2023-08-18T20:05:00Z" }, { + "quantity" : 94.575923891105845, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T21:30:00Z", - "quantity" : 150.94864870532584 + "startDate" : "2023-08-18T20:10:00Z" }, { + "quantity" : 94.906937578934446, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T21:35:00Z", - "quantity" : 150.98521164138711 + "startDate" : "2023-08-18T20:15:00Z" }, { + "quantity" : 95.255865275387578, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T21:40:00Z", - "quantity" : 151.00885216396446 + "startDate" : "2023-08-18T20:20:00Z" }, { + "quantity" : 95.623303645444025, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T21:45:00Z", - "quantity" : 151.01338818778473 + "startDate" : "2023-08-18T20:25:00Z" }, { + "quantity" : 96.000882373540378, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T21:50:00Z", - "quantity" : 150.99143437519743 + "startDate" : "2023-08-18T20:30:00Z" }, { + "quantity" : 96.365336203749791, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T21:55:00Z", - "quantity" : 150.93296955721272 + "startDate" : "2023-08-18T20:35:00Z" }, { + "quantity" : 96.715562621883805, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T22:00:00Z", - "quantity" : 150.82844495083614 + "startDate" : "2023-08-18T20:40:00Z" }, { + "quantity" : 97.051545329583519, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T22:05:00Z", - "quantity" : 150.67007248833207 + "startDate" : "2023-08-18T20:45:00Z" }, { + "quantity" : 97.373191168681046, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T22:10:00Z", - "quantity" : 150.44818289417267 + "startDate" : "2023-08-18T20:50:00Z" }, { + "quantity" : 97.68033912753441, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T22:15:00Z", - "quantity" : 150.15333032546724 + "startDate" : "2023-08-18T20:55:00Z" }, { + "quantity" : 97.972768583817071, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T22:20:00Z", - "quantity" : 149.77853346820271 + "startDate" : "2023-08-18T21:00:00Z" }, { + "quantity" : 98.250206839942507, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T22:25:00Z", - "quantity" : 149.31770786853204 + "startDate" : "2023-08-18T21:05:00Z" }, { + "quantity" : 98.512461657029007, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T22:30:00Z", - "quantity" : 148.76463863907512 + "startDate" : "2023-08-18T21:10:00Z" }, { + "quantity" : 98.759768446355963, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T22:35:00Z", - "quantity" : 148.11209303652518 + "startDate" : "2023-08-18T21:15:00Z" }, { + "quantity" : 98.992334190285419, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T22:40:00Z", - "quantity" : 147.3537767195518 + "startDate" : "2023-08-18T21:20:00Z" }, { + "quantity" : 99.210294956891744, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T22:45:00Z", - "quantity" : 146.48451003000551 + "startDate" : "2023-08-18T21:25:00Z" }, { + "quantity" : 99.413602382932424, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T22:50:00Z", - "quantity" : 145.49919962625873 + "startDate" : "2023-08-18T21:30:00Z" }, { + "quantity" : 99.601698280019434, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T22:55:00Z", - "quantity" : 144.39282841481932 + "startDate" : "2023-08-18T21:35:00Z" }, { + "quantity" : 99.773947375251453, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T23:00:00Z", - "quantity" : 143.1991839953188 + "startDate" : "2023-08-18T21:40:00Z" }, { + "quantity" : 99.929708236532747, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T23:05:00Z", - "quantity" : 142.08670682751759 + "startDate" : "2023-08-18T21:45:00Z" }, { + "quantity" : 100.06833834582778, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T23:10:00Z", - "quantity" : 141.07153230327131 + "startDate" : "2023-08-18T21:50:00Z" }, { + "quantity" : 100.18919669640144, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T23:15:00Z", - "quantity" : 140.14904110452417 + "startDate" : "2023-08-18T21:55:00Z" }, { + "quantity" : 100.29164609469677, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T23:20:00Z", - "quantity" : 139.31472998543964 + "startDate" : "2023-08-18T22:00:00Z" }, { + "quantity" : 100.3750551915391, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T23:25:00Z", - "quantity" : 138.56421620595611 + "startDate" : "2023-08-18T22:05:00Z" }, { + "quantity" : 100.43880026555911, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T23:30:00Z", - "quantity" : 137.8932410364304 + "startDate" : "2023-08-18T22:10:00Z" }, { + "quantity" : 100.48226678004278, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T23:35:00Z", - "quantity" : 137.29767242360586 + "startDate" : "2023-08-18T22:15:00Z" }, { + "quantity" : 100.50485073285557, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T23:40:00Z", - "quantity" : 136.77350690107906 + "startDate" : "2023-08-18T22:20:00Z" }, { + "quantity" : 100.50583426131806, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T23:45:00Z", - "quantity" : 136.3168708208772 + "startDate" : "2023-08-18T22:25:00Z" }, { + "quantity" : 100.48412374566237, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T23:50:00Z", - "quantity" : 135.92402097664686 + "startDate" : "2023-08-18T22:30:00Z" }, { + "quantity" : 100.4391400838507, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T23:55:00Z", - "quantity" : 135.59134468329685 + "startDate" : "2023-08-18T22:35:00Z" }, { + "quantity" : 100.3703782423454, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-30T00:00:00Z", - "quantity" : 135.31535937266145 + "startDate" : "2023-08-18T22:40:00Z" }, { + "quantity" : 100.28611412349363, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-30T00:05:00Z", - "quantity" : 135.09271175987229 + "startDate" : "2023-08-18T22:45:00Z" }, { + "quantity" : 100.20947967884376, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-30T00:10:00Z", - "quantity" : 134.92017663057632 + "startDate" : "2023-08-18T22:50:00Z" }, { + "quantity" : 100.14069735835136, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-30T00:15:00Z", - "quantity" : 134.79465529494851 + "startDate" : "2023-08-18T22:55:00Z" }, { + "quantity" : 100.07866920613634, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-30T00:20:00Z", - "quantity" : 134.71317375053073 + "startDate" : "2023-08-18T23:00:00Z" }, { + "quantity" : 100.02247162981837, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-30T00:25:00Z", - "quantity" : 134.67288059232183 + "startDate" : "2023-08-18T23:05:00Z" }, { + "quantity" : 99.971752267355782, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-30T00:30:00Z", - "quantity" : 134.66535701098488 + "startDate" : "2023-08-18T23:10:00Z" }, { + "quantity" : 99.926235939245828, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-30T01:20:54Z", - "quantity" : 134.66535701098488 + "startDate" : "2023-08-18T23:15:00Z" + }, + { + "quantity" : 99.88565546474166, + "quantityUnit" : "mg\/dL", + "startDate" : "2023-08-18T23:20:00Z" + }, + { + "quantity" : 99.849751805109122, + "quantityUnit" : "mg\/dL", + "startDate" : "2023-08-18T23:25:00Z" + }, + { + "quantity" : 99.81827416145336, + "quantityUnit" : "mg\/dL", + "startDate" : "2023-08-18T23:30:00Z" + }, + { + "quantity" : 99.791028333933468, + "quantityUnit" : "mg\/dL", + "startDate" : "2023-08-18T23:35:00Z" + }, + { + "quantity" : 99.767995772848735, + "quantityUnit" : "mg\/dL", + "startDate" : "2023-08-18T23:40:00Z" + }, + { + "quantity" : 99.748959024064902, + "quantityUnit" : "mg\/dL", + "startDate" : "2023-08-18T23:45:00Z" + }, + { + "quantity" : 99.733680926725469, + "quantityUnit" : "mg\/dL", + "startDate" : "2023-08-18T23:50:00Z" + }, + { + "quantity" : 99.721933417247726, + "quantityUnit" : "mg\/dL", + "startDate" : "2023-08-18T23:55:00Z" + }, + { + "quantity" : 99.713497415806842, + "quantityUnit" : "mg\/dL", + "startDate" : "2023-08-19T00:00:00Z" + }, + { + "quantity" : 99.708162692555675, + "quantityUnit" : "mg\/dL", + "startDate" : "2023-08-19T00:05:00Z" + }, + { + "quantity" : 99.705774231311352, + "quantityUnit" : "mg\/dL", + "startDate" : "2023-08-19T00:10:00Z" + }, + { + "quantity" : 99.705641639222222, + "quantityUnit" : "mg\/dL", + "startDate" : "2023-08-19T00:15:00Z" } ] diff --git a/LoopTests/Managers/LoopAlgorithmTests.swift b/LoopTests/Managers/LoopAlgorithmTests.swift index 76745cb1dc..3f6476c8c8 100644 --- a/LoopTests/Managers/LoopAlgorithmTests.swift +++ b/LoopTests/Managers/LoopAlgorithmTests.swift @@ -53,65 +53,12 @@ final class LoopAlgorithmTests: XCTestCase { // This matches the "testForecastFromLiveCaptureInputData" test of LoopDataManagerDosingTests, // Using the same input data, but generating the forecast using LoopPrediction - let mockGlucoseStore = MockGlucoseStore(for: .liveCapture) - let historicGlucose = mockGlucoseStore.storedGlucose! - - let mockDoseStore = MockDoseStore(for: .liveCapture) - let doses = mockDoseStore.doseHistory! - - let mockCarbStore = MockCarbStore(for: .liveCapture) - let carbEntries = mockCarbStore.carbHistory! - - let baseTime = historicGlucose.last!.startDate - let treatmentInterval = LoopAlgorithm.treatmentHistoryDateInterval(for: baseTime) - - - let isfStart = min(treatmentInterval.start, doses.map { $0.startDate }.min() ?? .distantFuture) - let isfEnd = baseTime.addingTimeInterval(InsulinMath.defaultInsulinActivityDuration).dateCeiledToTimeInterval(GlucoseMath.defaultDelta) - - let basalRateSchedule = loadBasalRateScheduleFixture("basal_profile") - - let insulinSensitivitySchedule = InsulinSensitivitySchedule( - unit: .milligramsPerDeciliter, - dailyItems: [ - RepeatingScheduleValue(startTime: 0, value: 45), - RepeatingScheduleValue(startTime: 32400, value: 55) - ], - timeZone: .utcTimeZone - )! - let carbRatioSchedule = CarbRatioSchedule( - unit: .gram(), - dailyItems: [ - RepeatingScheduleValue(startTime: 0.0, value: 10.0), - ], - timeZone: .utcTimeZone - )! - - let glucoseTargetRangeSchedule = GlucoseRangeSchedule( - unit: HKUnit.milligramsPerDeciliter, - dailyItems: [ - RepeatingScheduleValue(startTime: TimeInterval(0), value: DoubleRange(minValue: 100, maxValue: 110)), - RepeatingScheduleValue(startTime: TimeInterval(28800), value: DoubleRange(minValue: 90, maxValue: 100)), - RepeatingScheduleValue(startTime: TimeInterval(75600), value: DoubleRange(minValue: 100, maxValue: 110)) - ], - timeZone: .utcTimeZone)! - - let settings = LoopAlgorithmSettings( - basal: basalRateSchedule.between(start: treatmentInterval.start, end: treatmentInterval.end), - sensitivity: insulinSensitivitySchedule.quantitiesBetween(start: isfStart, end: isfEnd), - carbRatio: carbRatioSchedule.between(start: treatmentInterval.start, end: treatmentInterval.end), - target: glucoseTargetRangeSchedule.quantityBetween(start: baseTime, end: isfEnd), - maximumBasalRatePerHour: 5.0, - maximumBolus: 10, - suspendThreshold: GlucoseThreshold(unit: HKUnit.milligramsPerDeciliter, value: 75)) - - let input = LoopPredictionInput( - glucoseHistory: historicGlucose, - doses: doses, - carbEntries: carbEntries, - settings: settings) - - let prediction = try LoopAlgorithm.generatePrediction(input: input) + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + let url = bundle.url(forResource: "live_capture_input", withExtension: "json")! + let predictionInput = try! decoder.decode(LoopPredictionInput.self, from: try! Data(contentsOf: url)) + + let prediction = try LoopAlgorithm.generatePrediction(input: predictionInput) let expectedPredictedGlucose = loadPredictedGlucoseFixture("live_capture_predicted_glucose") @@ -127,11 +74,4 @@ final class LoopAlgorithmTests: XCTestCase { //XCTAssertEqual(1.99, recommendedBasal!.unitsPerHour, accuracy: defaultAccuracy) } - func testPerformanceExample() throws { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } - } - } diff --git a/LoopTests/Managers/LoopDataManagerDosingTests.swift b/LoopTests/Managers/LoopDataManagerDosingTests.swift index 621aa348a5..a1f26a0e92 100644 --- a/LoopTests/Managers/LoopDataManagerDosingTests.swift +++ b/LoopTests/Managers/LoopDataManagerDosingTests.swift @@ -55,7 +55,83 @@ class LoopDataManagerDosingTests: LoopDataManagerTests { // MARK: Tests func testForecastFromLiveCaptureInputData() { - setUp(for: .liveCapture) + + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + let url = bundle.url(forResource: "live_capture_input", withExtension: "json")! + let predictionInput = try! decoder.decode(LoopPredictionInput.self, from: try! Data(contentsOf: url)) + + // Therapy settings in the "live capture" input only have one value, so we can fake some schedules + // from the first entry of each therapy setting's history. + let basalRateSchedule = BasalRateSchedule(dailyItems: [ + RepeatingScheduleValue(startTime: 0, value: predictionInput.settings.basal.first!.value) + ]) + let insulinSensitivitySchedule = InsulinSensitivitySchedule( + unit: .milligramsPerDeciliter, + dailyItems: [ + RepeatingScheduleValue(startTime: 0, value: predictionInput.settings.sensitivity.first!.value.doubleValue(for: .milligramsPerDeciliter)) + ], + timeZone: .utcTimeZone + )! + let carbRatioSchedule = CarbRatioSchedule( + unit: .gram(), + dailyItems: [ + RepeatingScheduleValue(startTime: 0.0, value: predictionInput.settings.carbRatio.first!.value) + ], + timeZone: .utcTimeZone + )! + + let settings = LoopSettings( + dosingEnabled: false, + glucoseTargetRangeSchedule: glucoseTargetRangeSchedule, + insulinSensitivitySchedule: insulinSensitivitySchedule, + basalRateSchedule: basalRateSchedule, + carbRatioSchedule: carbRatioSchedule, + maximumBasalRatePerHour: 10, + maximumBolus: 5, + suspendThreshold: predictionInput.settings.suspendThreshold, + automaticDosingStrategy: .automaticBolus + ) + + let glucoseStore = MockGlucoseStore() + glucoseStore.storedGlucose = predictionInput.glucoseHistory + + let currentDate = glucoseStore.latestGlucose!.startDate + now = currentDate + + let doseStore = MockDoseStore() + doseStore.basalProfile = basalRateSchedule + doseStore.basalProfileApplyingOverrideHistory = doseStore.basalProfile + doseStore.sensitivitySchedule = insulinSensitivitySchedule + doseStore.doseHistory = predictionInput.doses + doseStore.lastAddedPumpData = predictionInput.doses.last!.startDate + let carbStore = MockCarbStore() + carbStore.insulinSensitivityScheduleApplyingOverrideHistory = insulinSensitivitySchedule + carbStore.carbRatioSchedule = carbRatioSchedule + carbStore.carbRatioScheduleApplyingOverrideHistory = carbRatioSchedule + carbStore.carbHistory = predictionInput.carbEntries + + + dosingDecisionStore = MockDosingDecisionStore() + automaticDosingStatus = AutomaticDosingStatus(automaticDosingEnabled: true, isAutomaticDosingAllowed: true) + loopDataManager = LoopDataManager( + lastLoopCompleted: currentDate, + basalDeliveryState: .active(currentDate), + settings: settings, + overrideHistory: TemporaryScheduleOverrideHistory(), + analyticsServicesManager: AnalyticsServicesManager(), + localCacheDuration: .days(1), + doseStore: doseStore, + glucoseStore: glucoseStore, + carbStore: carbStore, + dosingDecisionStore: dosingDecisionStore, + latestStoredSettingsProvider: MockLatestStoredSettingsProvider(), + now: { currentDate }, + pumpInsulinType: .novolog, + automaticDosingStatus: automaticDosingStatus, + trustedTimeOffset: { 0 } + ) + let expectedPredictedGlucose = loadPredictedGlucoseFixture("live_capture_predicted_glucose") let updateGroup = DispatchGroup() @@ -63,7 +139,7 @@ class LoopDataManagerDosingTests: LoopDataManagerTests { var predictedGlucose: [PredictedGlucoseValue]? var recommendedBasal: TempBasalRecommendation? self.loopDataManager.getLoopState { _, state in - predictedGlucose = state.predictedGlucose + predictedGlucose = state.predictedGlucoseIncludingPendingInsulin recommendedBasal = state.recommendedAutomaticDose?.recommendation.basalAdjustment updateGroup.leave() } @@ -71,14 +147,13 @@ class LoopDataManagerDosingTests: LoopDataManagerTests { updateGroup.wait() XCTAssertNotNil(predictedGlucose) + XCTAssertEqual(expectedPredictedGlucose.count, predictedGlucose!.count) for (expected, calculated) in zip(expectedPredictedGlucose, predictedGlucose!) { XCTAssertEqual(expected.startDate, calculated.startDate) XCTAssertEqual(expected.quantity.doubleValue(for: .milligramsPerDeciliter), calculated.quantity.doubleValue(for: .milligramsPerDeciliter), accuracy: defaultAccuracy) } - - XCTAssertEqual(1.99, recommendedBasal!.unitsPerHour, accuracy: defaultAccuracy) } diff --git a/LoopTests/Mock Stores/MockDoseStore.swift b/LoopTests/Mock Stores/MockDoseStore.swift index a676fa9e74..dc358ec361 100644 --- a/LoopTests/Mock Stores/MockDoseStore.swift +++ b/LoopTests/Mock Stores/MockDoseStore.swift @@ -41,7 +41,7 @@ class MockDoseStore: DoseStoreProtocol { // Default to the adult exponential insulin model var insulinModelProvider: InsulinModelProvider = StaticInsulinModelProvider(ExponentialInsulinModelPreset.rapidActingAdult) - var longestEffectDuration: TimeInterval = ExponentialInsulinModelPreset.rapidActingAdult.actionDuration + var longestEffectDuration: TimeInterval = ExponentialInsulinModelPreset.rapidActingAdult.effectDuration var insulinSensitivitySchedule: InsulinSensitivitySchedule? @@ -92,7 +92,7 @@ class MockDoseStore: DoseStoreProtocol { } func getGlucoseEffects(start: Date, end: Date? = nil, basalDosingEnd: Date? = Date(), completion: @escaping (_ result: DoseStoreResult<[GlucoseEffect]>) -> Void) { - if let doseHistory, let sensitivitySchedule { + if let doseHistory, let sensitivitySchedule, let basalProfile = basalProfileApplyingOverrideHistory { // To properly know glucose effects at startDate, we need to go back another DIA hours let doseStart = start.addingTimeInterval(-longestEffectDuration) let doses = doseHistory.filterDateRange(doseStart, end) @@ -103,7 +103,9 @@ class MockDoseStore: DoseStoreProtocol { return dose.trimmed(to: basalDosingEnd) } - let glucoseEffects = trimmedDoses.glucoseEffects(insulinModelProvider: self.insulinModelProvider, longestEffectDuration: self.longestEffectDuration, insulinSensitivity: sensitivitySchedule, from: start, to: end) + let annotatedDoses = trimmedDoses.annotated(with: basalProfile) + + let glucoseEffects = annotatedDoses.glucoseEffects(insulinModelProvider: self.insulinModelProvider, longestEffectDuration: self.longestEffectDuration, insulinSensitivity: sensitivitySchedule, from: start, to: end) completion(.success(glucoseEffects.filterDateRange(start, end))) } else { return completion(.success(getCannedGlucoseEffects())) diff --git a/LoopTests/Mock Stores/MockGlucoseStore.swift b/LoopTests/Mock Stores/MockGlucoseStore.swift index 93b96f176b..19a6bc22e8 100644 --- a/LoopTests/Mock Stores/MockGlucoseStore.swift +++ b/LoopTests/Mock Stores/MockGlucoseStore.swift @@ -12,7 +12,7 @@ import LoopKit class MockGlucoseStore: GlucoseStoreProtocol { - init(for scenario: DosingTestScenario) { + init(for scenario: DosingTestScenario = .flatAndStable) { self.scenario = scenario // The store returns different effect values based on the scenario storedGlucose = loadHistoricGlucose(scenario: scenario) } @@ -80,12 +80,12 @@ class MockGlucoseStore: GlucoseStoreProtocol { } func counteractionEffects(for samples: [Sample], to effects: [GlucoseEffect]) -> [GlucoseEffectVelocity] where Sample : GlucoseSampleValue { - return [] // TODO: check if we'll ever want to test this + samples.counteractionEffects(to: effects) } - func getRecentMomentumEffect(_ completion: @escaping (_ effects: Result<[GlucoseEffect], Error>) -> Void) { + func getRecentMomentumEffect(for date: Date? = nil, _ completion: @escaping (_ effects: Result<[GlucoseEffect], Error>) -> Void) { if let storedGlucose { - let samples = storedGlucose.filterDateRange(scenario.currentDate.addingTimeInterval(-GlucoseMath.momentumDataInterval), nil) + let samples = storedGlucose.filterDateRange((date ?? Date()).addingTimeInterval(-GlucoseMath.momentumDataInterval), nil) completion(.success(samples.linearMomentumEffect())) } else { let fixture: [JSONDictionary] = loadFixture(momentumEffectToLoad) From a5f44a256ef1dba8ef4347706260eaf83396fe72 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Sat, 2 Sep 2023 14:24:01 -0500 Subject: [PATCH 20/49] Cleanup --- Loop.xcodeproj/project.pbxproj | 12 - .../live_capture_carb_entries.json | 6 - .../live_capture/live_capture_doses.json | 793 ---------------- .../live_capture_historic_glucose.json | 854 ------------------ LoopTests/Managers/LoopAlgorithmTests.swift | 2 - 5 files changed, 1667 deletions(-) delete mode 100644 LoopTests/Fixtures/live_capture/live_capture_carb_entries.json delete mode 100644 LoopTests/Fixtures/live_capture/live_capture_doses.json delete mode 100644 LoopTests/Fixtures/live_capture/live_capture_historic_glucose.json diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 78f53ccf42..a5426f692c 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -414,9 +414,6 @@ C1201E2C23ECDBD0002DA84A /* WatchContextRequestUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1201E2B23ECDBD0002DA84A /* WatchContextRequestUserInfo.swift */; }; C1201E2D23ECDF3D002DA84A /* WatchContextRequestUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1201E2B23ECDBD0002DA84A /* WatchContextRequestUserInfo.swift */; }; C13072BA2A76AF31009A7C58 /* live_capture_predicted_glucose.json in Resources */ = {isa = PBXBuildFile; fileRef = C13072B92A76AF31009A7C58 /* live_capture_predicted_glucose.json */; }; - C13072BE2A76AF97009A7C58 /* live_capture_doses.json in Resources */ = {isa = PBXBuildFile; fileRef = C13072BD2A76AF97009A7C58 /* live_capture_doses.json */; }; - C13072C02A76B041009A7C58 /* live_capture_carb_entries.json in Resources */ = {isa = PBXBuildFile; fileRef = C13072BF2A76B041009A7C58 /* live_capture_carb_entries.json */; }; - C13072C42A76B0B1009A7C58 /* live_capture_historic_glucose.json in Resources */ = {isa = PBXBuildFile; fileRef = C13072C32A76B0B1009A7C58 /* live_capture_historic_glucose.json */; }; C13255D6223E7BE2008AF50C /* BolusProgressTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = C1F8B1DB223862D500DD66CF /* BolusProgressTableViewCell.xib */; }; C13DA2B024F6C7690098BB29 /* UIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13DA2AF24F6C7690098BB29 /* UIViewController.swift */; }; C148CEE724FD91BD00711B3B /* DeliveryUncertaintyAlertManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C148CEE624FD91BD00711B3B /* DeliveryUncertaintyAlertManager.swift */; }; @@ -1422,9 +1419,6 @@ C12CB9B623106A6200F84978 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/Intents.strings; sourceTree = ""; }; C12CB9B823106A6300F84978 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Intents.strings; sourceTree = ""; }; C13072B92A76AF31009A7C58 /* live_capture_predicted_glucose.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = live_capture_predicted_glucose.json; sourceTree = ""; }; - C13072BD2A76AF97009A7C58 /* live_capture_doses.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = live_capture_doses.json; sourceTree = ""; }; - C13072BF2A76B041009A7C58 /* live_capture_carb_entries.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = live_capture_carb_entries.json; sourceTree = ""; }; - C13072C32A76B0B1009A7C58 /* live_capture_historic_glucose.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = live_capture_historic_glucose.json; sourceTree = ""; }; C13DA2AF24F6C7690098BB29 /* UIViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewController.swift; sourceTree = ""; }; C148CEE624FD91BD00711B3B /* DeliveryUncertaintyAlertManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeliveryUncertaintyAlertManager.swift; sourceTree = ""; }; C14952142995822A0095AA84 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -2760,9 +2754,6 @@ isa = PBXGroup; children = ( C13072B92A76AF31009A7C58 /* live_capture_predicted_glucose.json */, - C13072BD2A76AF97009A7C58 /* live_capture_doses.json */, - C13072BF2A76B041009A7C58 /* live_capture_carb_entries.json */, - C13072C32A76B0B1009A7C58 /* live_capture_historic_glucose.json */, C16FC0AF2A99392F0025E239 /* live_capture_input.json */, ); path = live_capture; @@ -3418,7 +3409,6 @@ E93E86CE24E2E02200FF40C8 /* high_and_stable_momentum_effect.json in Resources */, C13072BA2A76AF31009A7C58 /* live_capture_predicted_glucose.json in Resources */, E93E865424DB6CBA00FF40C8 /* retrospective_output.json in Resources */, - C13072BE2A76AF97009A7C58 /* live_capture_doses.json in Resources */, E9C58A7F24DB529A00487A17 /* counteraction_effect_falling_glucose.json in Resources */, E93E865624DB731900FF40C8 /* predicted_glucose_without_retrospective.json in Resources */, E9B3553D293706CB0076AB04 /* long_interval_counteraction_effect.json in Resources */, @@ -3431,7 +3421,6 @@ E90909D224E34AC500F963D2 /* high_and_rising_with_cob_insulin_effect.json in Resources */, E90909F224E35B4D00F963D2 /* high_and_falling_counteraction_effect.json in Resources */, E90909EE24E35B4000F963D2 /* high_and_falling_predicted_glucose.json in Resources */, - C13072C02A76B041009A7C58 /* live_capture_carb_entries.json in Resources */, E90909DD24E34F1600F963D2 /* low_and_falling_carb_effect.json in Resources */, E90909E924E3530200F963D2 /* low_with_low_treatment_predicted_glucose.json in Resources */, E90909D124E34AC500F963D2 /* high_and_rising_with_cob_momentum_effect.json in Resources */, @@ -3458,7 +3447,6 @@ E93E86C324E1FE6100FF40C8 /* flat_and_stable_counteraction_effect.json in Resources */, E9C58A7E24DB529A00487A17 /* dynamic_glucose_effect_partially_observed.json in Resources */, E90909F324E35B4D00F963D2 /* high_and_falling_carb_effect.json in Resources */, - C13072C42A76B0B1009A7C58 /* live_capture_historic_glucose.json in Resources */, E93E86CA24E2E02200FF40C8 /* high_and_stable_insulin_effect.json in Resources */, E93E86BB24E1FDC400FF40C8 /* flat_and_stable_momentum_effect.json in Resources */, E93E86CB24E2E02200FF40C8 /* high_and_stable_carb_effect.json in Resources */, diff --git a/LoopTests/Fixtures/live_capture/live_capture_carb_entries.json b/LoopTests/Fixtures/live_capture/live_capture_carb_entries.json deleted file mode 100644 index 6c5b77202e..0000000000 --- a/LoopTests/Fixtures/live_capture/live_capture_carb_entries.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "startDate": "2023-07-29T18:17:07Z", - "quantity": 50 - } -] diff --git a/LoopTests/Fixtures/live_capture/live_capture_doses.json b/LoopTests/Fixtures/live_capture/live_capture_doses.json deleted file mode 100644 index 452fce1d0a..0000000000 --- a/LoopTests/Fixtures/live_capture/live_capture_doses.json +++ /dev/null @@ -1,793 +0,0 @@ -[ - { - "value": 0, - "startDate": "2023-07-28T19:15:55Z", - "type": "tempBasal", - "endDate": "2023-07-28T19:15:55Z", - "unit": "U/hour" - }, - { - "value": 0.20000000000000001, - "startDate": "2023-07-28T19:35:49Z", - "type": "tempBasal", - "endDate": "2023-07-28T19:35:49Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-28T19:40:49Z", - "type": "tempBasal", - "endDate": "2023-07-28T19:40:49Z", - "unit": "U/hour" - }, - { - "value": 0.25, - "startDate": "2023-07-28T20:00:49Z", - "type": "bolus", - "endDate": "2023-07-28T20:00:49Z", - "unit": "U" - }, - { - "value": 0.25, - "startDate": "2023-07-28T20:05:48Z", - "type": "bolus", - "endDate": "2023-07-28T20:05:48Z", - "unit": "U" - }, - { - "value": 0.40000000000000002, - "startDate": "2023-07-28T20:10:51Z", - "type": "bolus", - "endDate": "2023-07-28T20:10:51Z", - "unit": "U" - }, - { - "value": 0.34999999999999998, - "startDate": "2023-07-28T20:15:49Z", - "type": "bolus", - "endDate": "2023-07-28T20:15:49Z", - "unit": "U" - }, - { - "value": 0.5, - "startDate": "2023-07-28T20:20:52Z", - "type": "bolus", - "endDate": "2023-07-28T20:20:52Z", - "unit": "U" - }, - { - "value": 0.40000000000000002, - "startDate": "2023-07-28T20:30:48Z", - "type": "bolus", - "endDate": "2023-07-28T20:30:48Z", - "unit": "U" - }, - { - "value": 0.10000000000000001, - "startDate": "2023-07-28T20:35:51Z", - "type": "bolus", - "endDate": "2023-07-28T20:35:51Z", - "unit": "U" - }, - { - "value": 0.25, - "startDate": "2023-07-28T20:40:51Z", - "type": "bolus", - "endDate": "2023-07-28T20:40:51Z", - "unit": "U" - }, - { - "value": 0.80000000000000004, - "startDate": "2023-07-28T20:45:56Z", - "type": "tempBasal", - "endDate": "2023-07-28T20:45:56Z", - "unit": "U/hour" - }, - { - "value": 0.40000000000000002, - "startDate": "2023-07-28T20:55:48Z", - "type": "tempBasal", - "endDate": "2023-07-28T20:55:48Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-28T20:59:35Z", - "type": "suspend", - "endDate": "2023-07-28T20:59:35Z", - "unit": "U" - }, - { - "value": 3.0499999999999998, - "startDate": "2023-07-29T00:07:52Z", - "type": "bolus", - "endDate": "2023-07-29T00:07:52Z", - "unit": "U" - }, - { - "value": 0.40000000000000002, - "startDate": "2023-07-29T00:10:54Z", - "type": "bolus", - "endDate": "2023-07-29T00:10:54Z", - "unit": "U" - }, - { - "value": 0.5, - "startDate": "2023-07-29T00:15:49Z", - "type": "bolus", - "endDate": "2023-07-29T00:15:49Z", - "unit": "U" - }, - { - "value": 0.40000000000000002, - "startDate": "2023-07-29T00:20:50Z", - "type": "bolus", - "endDate": "2023-07-29T00:20:50Z", - "unit": "U" - }, - { - "value": 0.40000000000000002, - "startDate": "2023-07-29T00:25:49Z", - "type": "bolus", - "endDate": "2023-07-29T00:25:49Z", - "unit": "U" - }, - { - "value": 0.55000000000000004, - "startDate": "2023-07-29T00:30:52Z", - "type": "bolus", - "endDate": "2023-07-29T00:30:52Z", - "unit": "U" - }, - { - "value": 0.59999999999999998, - "startDate": "2023-07-29T00:35:50Z", - "type": "bolus", - "endDate": "2023-07-29T00:35:50Z", - "unit": "U" - }, - { - "value": 0.40000000000000002, - "startDate": "2023-07-29T00:40:50Z", - "type": "bolus", - "endDate": "2023-07-29T00:40:50Z", - "unit": "U" - }, - { - "value": 0.14999999999999999, - "startDate": "2023-07-29T00:45:49Z", - "type": "bolus", - "endDate": "2023-07-29T00:45:49Z", - "unit": "U" - }, - { - "value": 0.10000000000000001, - "startDate": "2023-07-29T00:55:50Z", - "type": "tempBasal", - "endDate": "2023-07-29T00:55:50Z", - "unit": "U/hour" - }, - { - "value": 0.29999999999999999, - "startDate": "2023-07-29T01:00:49Z", - "type": "tempBasal", - "endDate": "2023-07-29T01:00:49Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T01:05:49Z", - "type": "tempBasal", - "endDate": "2023-07-29T01:05:49Z", - "unit": "U/hour" - }, - { - "value": 0.14999999999999999, - "startDate": "2023-07-29T01:15:49Z", - "type": "tempBasal", - "endDate": "2023-07-29T01:15:49Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T01:20:50Z", - "type": "tempBasal", - "endDate": "2023-07-29T01:20:50Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T01:45:49Z", - "type": "tempBasal", - "endDate": "2023-07-29T01:45:49Z", - "unit": "U/hour" - }, - { - "value": 0.050000000000000003, - "startDate": "2023-07-29T01:50:50Z", - "type": "bolus", - "endDate": "2023-07-29T01:50:50Z", - "unit": "U" - }, - { - "value": 0.34999999999999998, - "startDate": "2023-07-29T02:00:58Z", - "type": "tempBasal", - "endDate": "2023-07-29T02:00:58Z", - "unit": "U/hour" - }, - { - "value": 0.050000000000000003, - "startDate": "2023-07-29T02:00:59Z", - "type": "tempBasal", - "endDate": "2023-07-29T02:00:59Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T02:06:04Z", - "type": "tempBasal", - "endDate": "2023-07-29T02:06:04Z", - "unit": "U/hour" - }, - { - "value": 0.20000000000000001, - "startDate": "2023-07-29T02:10:55Z", - "type": "bolus", - "endDate": "2023-07-29T02:10:55Z", - "unit": "U" - }, - { - "value": 0.10000000000000001, - "startDate": "2023-07-29T02:15:49Z", - "type": "bolus", - "endDate": "2023-07-29T02:15:49Z", - "unit": "U" - }, - { - "value": 0.10000000000000001, - "startDate": "2023-07-29T02:21:02Z", - "type": "bolus", - "endDate": "2023-07-29T02:21:02Z", - "unit": "U" - }, - { - "value": 0.20000000000000001, - "startDate": "2023-07-29T02:30:49Z", - "type": "bolus", - "endDate": "2023-07-29T02:30:49Z", - "unit": "U" - }, - { - "value": 0.25, - "startDate": "2023-07-29T02:35:55Z", - "type": "bolus", - "endDate": "2023-07-29T02:35:55Z", - "unit": "U" - }, - { - "value": 0.5, - "startDate": "2023-07-29T02:45:58Z", - "type": "bolus", - "endDate": "2023-07-29T02:45:58Z", - "unit": "U" - }, - { - "value": 0.29999999999999999, - "startDate": "2023-07-29T02:50:54Z", - "type": "bolus", - "endDate": "2023-07-29T02:50:54Z", - "unit": "U" - }, - { - "value": 0.25, - "startDate": "2023-07-29T02:55:59Z", - "type": "bolus", - "endDate": "2023-07-29T02:55:59Z", - "unit": "U" - }, - { - "value": 0.14999999999999999, - "startDate": "2023-07-29T03:00:49Z", - "type": "bolus", - "endDate": "2023-07-29T03:00:49Z", - "unit": "U" - }, - { - "value": 0.050000000000000003, - "startDate": "2023-07-29T03:05:51Z", - "type": "bolus", - "endDate": "2023-07-29T03:05:51Z", - "unit": "U" - }, - { - "value": 0.14999999999999999, - "startDate": "2023-07-29T03:10:58Z", - "type": "bolus", - "endDate": "2023-07-29T03:10:58Z", - "unit": "U" - }, - { - "value": 0.050000000000000003, - "startDate": "2023-07-29T03:20:59Z", - "type": "bolus", - "endDate": "2023-07-29T03:20:59Z", - "unit": "U" - }, - { - "value": 0.20000000000000001, - "startDate": "2023-07-29T03:30:50Z", - "type": "bolus", - "endDate": "2023-07-29T03:30:50Z", - "unit": "U" - }, - { - "value": 0.14999999999999999, - "startDate": "2023-07-29T03:35:51Z", - "type": "bolus", - "endDate": "2023-07-29T03:35:51Z", - "unit": "U" - }, - { - "value": 0.25, - "startDate": "2023-07-29T03:45:51Z", - "type": "tempBasal", - "endDate": "2023-07-29T03:45:51Z", - "unit": "U/hour" - }, - { - "value": 0.45000000000000001, - "startDate": "2023-07-29T03:50:50Z", - "type": "tempBasal", - "endDate": "2023-07-29T03:50:50Z", - "unit": "U/hour" - }, - { - "value": 0.10000000000000001, - "startDate": "2023-07-29T03:55:49Z", - "type": "tempBasal", - "endDate": "2023-07-29T03:55:49Z", - "unit": "U/hour" - }, - { - "value": 0.29999999999999999, - "startDate": "2023-07-29T04:00:50Z", - "type": "tempBasal", - "endDate": "2023-07-29T04:00:50Z", - "unit": "U/hour" - }, - { - "value": 0.14999999999999999, - "startDate": "2023-07-29T04:05:51Z", - "type": "tempBasal", - "endDate": "2023-07-29T04:05:51Z", - "unit": "U/hour" - }, - { - "value": 0.5, - "startDate": "2023-07-29T04:10:49Z", - "type": "tempBasal", - "endDate": "2023-07-29T04:10:49Z", - "unit": "U/hour" - }, - { - "value": 0.34999999999999998, - "startDate": "2023-07-29T04:25:49Z", - "type": "tempBasal", - "endDate": "2023-07-29T04:25:49Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T04:36:12Z", - "type": "tempBasal", - "endDate": "2023-07-29T04:36:12Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T05:00:49Z", - "type": "tempBasal", - "endDate": "2023-07-29T05:00:49Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T05:20:53Z", - "type": "tempBasal", - "endDate": "2023-07-29T05:20:53Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T05:45:52Z", - "type": "tempBasal", - "endDate": "2023-07-29T05:45:52Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T06:10:49Z", - "type": "tempBasal", - "endDate": "2023-07-29T06:10:49Z", - "unit": "U/hour" - }, - { - "value": 1.05, - "startDate": "2023-07-29T06:40:50Z", - "type": "bolus", - "endDate": "2023-07-29T06:40:50Z", - "unit": "U" - }, - { - "value": 0.69999999999999996, - "startDate": "2023-07-29T06:45:49Z", - "type": "bolus", - "endDate": "2023-07-29T06:45:49Z", - "unit": "U" - }, - { - "value": 0.40000000000000002, - "startDate": "2023-07-29T06:50:50Z", - "type": "bolus", - "endDate": "2023-07-29T06:50:50Z", - "unit": "U" - }, - { - "value": 0.59999999999999998, - "startDate": "2023-07-29T06:55:52Z", - "type": "bolus", - "endDate": "2023-07-29T06:55:52Z", - "unit": "U" - }, - { - "value": 0.5, - "startDate": "2023-07-29T07:00:49Z", - "type": "bolus", - "endDate": "2023-07-29T07:00:49Z", - "unit": "U" - }, - { - "value": 0.80000000000000004, - "startDate": "2023-07-29T07:05:52Z", - "type": "bolus", - "endDate": "2023-07-29T07:05:52Z", - "unit": "U" - }, - { - "value": 0.69999999999999996, - "startDate": "2023-07-29T07:10:51Z", - "type": "bolus", - "endDate": "2023-07-29T07:10:51Z", - "unit": "U" - }, - { - "value": 0.10000000000000001, - "startDate": "2023-07-29T07:15:50Z", - "type": "bolus", - "endDate": "2023-07-29T07:15:50Z", - "unit": "U" - }, - { - "value": 0, - "startDate": "2023-07-29T07:20:50Z", - "type": "tempBasal", - "endDate": "2023-07-29T07:20:50Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T07:45:58Z", - "type": "tempBasal", - "endDate": "2023-07-29T07:45:58Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T08:10:53Z", - "type": "tempBasal", - "endDate": "2023-07-29T08:10:53Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T08:30:55Z", - "type": "tempBasal", - "endDate": "2023-07-29T08:30:55Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T08:55:56Z", - "type": "tempBasal", - "endDate": "2023-07-29T08:55:56Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T09:20:52Z", - "type": "tempBasal", - "endDate": "2023-07-29T09:20:52Z", - "unit": "U/hour" - }, - { - "value": 0.10000000000000001, - "startDate": "2023-07-29T10:00:51Z", - "type": "bolus", - "endDate": "2023-07-29T10:00:51Z", - "unit": "U" - }, - { - "value": 0.050000000000000003, - "startDate": "2023-07-29T10:05:58Z", - "type": "bolus", - "endDate": "2023-07-29T10:05:58Z", - "unit": "U" - }, - { - "value": 0.050000000000000003, - "startDate": "2023-07-29T10:20:52Z", - "type": "bolus", - "endDate": "2023-07-29T10:20:52Z", - "unit": "U" - }, - { - "value": 0.050000000000000003, - "startDate": "2023-07-29T10:25:50Z", - "type": "bolus", - "endDate": "2023-07-29T10:25:50Z", - "unit": "U" - }, - { - "value": 0.050000000000000003, - "startDate": "2023-07-29T10:30:53Z", - "type": "bolus", - "endDate": "2023-07-29T10:30:53Z", - "unit": "U" - }, - { - "value": 0, - "startDate": "2023-07-29T11:25:51Z", - "type": "tempBasal", - "endDate": "2023-07-29T11:25:51Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T12:50:53Z", - "type": "tempBasal", - "endDate": "2023-07-29T12:50:53Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T13:00:54Z", - "type": "tempBasal", - "endDate": "2023-07-29T13:00:54Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T13:25:52Z", - "type": "tempBasal", - "endDate": "2023-07-29T13:25:52Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T13:50:53Z", - "type": "tempBasal", - "endDate": "2023-07-29T13:50:53Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T14:15:53Z", - "type": "tempBasal", - "endDate": "2023-07-29T14:15:53Z", - "unit": "U/hour" - }, - { - "value": 0.65000000000000002, - "startDate": "2023-07-29T15:00:51Z", - "type": "bolus", - "endDate": "2023-07-29T15:00:51Z", - "unit": "U" - }, - { - "value": 0.65000000000000002, - "startDate": "2023-07-29T15:05:51Z", - "type": "bolus", - "endDate": "2023-07-29T15:05:51Z", - "unit": "U" - }, - { - "value": 0.20000000000000001, - "startDate": "2023-07-29T15:10:53Z", - "type": "bolus", - "endDate": "2023-07-29T15:10:53Z", - "unit": "U" - }, - { - "value": 0.40000000000000002, - "startDate": "2023-07-29T15:15:51Z", - "type": "bolus", - "endDate": "2023-07-29T15:15:51Z", - "unit": "U" - }, - { - "value": 0.29999999999999999, - "startDate": "2023-07-29T15:21:14Z", - "type": "bolus", - "endDate": "2023-07-29T15:21:14Z", - "unit": "U" - }, - { - "value": 0.69999999999999996, - "startDate": "2023-07-29T15:30:51Z", - "type": "tempBasal", - "endDate": "2023-07-29T15:30:51Z", - "unit": "U/hour" - }, - { - "value": 0.40000000000000002, - "startDate": "2023-07-29T15:35:53Z", - "type": "bolus", - "endDate": "2023-07-29T15:35:53Z", - "unit": "U" - }, - { - "value": 0.59999999999999998, - "startDate": "2023-07-29T15:40:51Z", - "type": "bolus", - "endDate": "2023-07-29T15:40:51Z", - "unit": "U" - }, - { - "value": 0.10000000000000001, - "startDate": "2023-07-29T15:45:52Z", - "type": "bolus", - "endDate": "2023-07-29T15:45:52Z", - "unit": "U" - }, - { - "value": 0.40000000000000002, - "startDate": "2023-07-29T15:50:50Z", - "type": "bolus", - "endDate": "2023-07-29T15:50:50Z", - "unit": "U" - }, - { - "value": 0.59999999999999998, - "startDate": "2023-07-29T15:55:53Z", - "type": "bolus", - "endDate": "2023-07-29T15:55:53Z", - "unit": "U" - }, - { - "value": 0.20000000000000001, - "startDate": "2023-07-29T16:00:51Z", - "type": "bolus", - "endDate": "2023-07-29T16:00:51Z", - "unit": "U" - }, - { - "value": 0.10000000000000001, - "startDate": "2023-07-29T16:05:51Z", - "type": "bolus", - "endDate": "2023-07-29T16:05:51Z", - "unit": "U" - }, - { - "value": 0.25, - "startDate": "2023-07-29T16:15:51Z", - "type": "bolus", - "endDate": "2023-07-29T16:15:51Z", - "unit": "U" - }, - { - "value": 0.20000000000000001, - "startDate": "2023-07-29T16:20:50Z", - "type": "bolus", - "endDate": "2023-07-29T16:20:50Z", - "unit": "U" - }, - { - "value": 0, - "startDate": "2023-07-29T16:25:54Z", - "type": "tempBasal", - "endDate": "2023-07-29T16:25:54Z", - "unit": "U/hour" - }, - { - "value": 0.29999999999999999, - "startDate": "2023-07-29T16:55:51Z", - "type": "tempBasal", - "endDate": "2023-07-29T16:55:51Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T17:00:51Z", - "type": "tempBasal", - "endDate": "2023-07-29T17:00:51Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T17:25:54Z", - "type": "tempBasal", - "endDate": "2023-07-29T17:25:54Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T17:45:56Z", - "type": "tempBasal", - "endDate": "2023-07-29T17:45:56Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T18:10:56Z", - "type": "tempBasal", - "endDate": "2023-07-29T18:10:56Z", - "unit": "U/hour" - }, - { - "value": 0.20000000000000001, - "startDate": "2023-07-29T18:15:56Z", - "type": "bolus", - "endDate": "2023-07-29T18:15:56Z", - "unit": "U" - }, - { - "value": 4.0999999999999996, - "startDate": "2023-07-29T18:17:11Z", - "type": "bolus", - "endDate": "2023-07-29T18:17:11Z", - "unit": "U" - }, - { - "value": 0.29999999999999999, - "startDate": "2023-07-29T18:25:55Z", - "type": "bolus", - "endDate": "2023-07-29T18:25:55Z", - "unit": "U" - }, - { - "value": 0, - "startDate": "2023-07-29T18:40:51Z", - "type": "tempBasal", - "endDate": "2023-07-29T18:40:51Z", - "unit": "U/hour" - }, - { - "value": 0.34999999999999998, - "startDate": "2023-07-29T18:51:05Z", - "type": "tempBasal", - "endDate": "2023-07-29T18:51:05Z", - "unit": "U/hour" - }, - { - "value": 0.55000000000000004, - "startDate": "2023-07-29T18:51:07Z", - "type": "tempBasal", - "endDate": "2023-07-29T18:51:07Z", - "unit": "U/hour" - }, - { - "value": 0.20000000000000001, - "startDate": "2023-07-29T18:55:55Z", - "type": "tempBasal", - "endDate": "2023-07-29T18:55:55Z", - "unit": "U/hour" - }, - { - "value": 0.5, - "startDate": "2023-07-29T19:00:57Z", - "type": "tempBasal", - "endDate": "2023-07-29T19:00:57Z", - "unit": "U/hour" - } -] diff --git a/LoopTests/Fixtures/live_capture/live_capture_historic_glucose.json b/LoopTests/Fixtures/live_capture/live_capture_historic_glucose.json deleted file mode 100644 index 0da74e48fc..0000000000 --- a/LoopTests/Fixtures/live_capture/live_capture_historic_glucose.json +++ /dev/null @@ -1,854 +0,0 @@ -[ - { - "startDate": "2023-07-29T01:40:51Z", - "quantity": 254 - }, - { - "startDate": "2023-07-29T01:45:52Z", - "quantity": 259 - }, - { - "startDate": "2023-07-29T01:50:51Z", - "quantity": 252 - }, - { - "startDate": "2023-07-29T01:55:52Z", - "quantity": 250 - }, - { - "startDate": "2023-07-29T02:00:52Z", - "quantity": 234 - }, - { - "startDate": "2023-07-29T02:05:51Z", - "quantity": 222 - }, - { - "startDate": "2023-07-29T02:10:51Z", - "quantity": 233 - }, - { - "startDate": "2023-07-29T02:15:52Z", - "quantity": 235 - }, - { - "startDate": "2023-07-29T02:20:52Z", - "quantity": 231 - }, - { - "startDate": "2023-07-29T02:25:52Z", - "quantity": 235 - }, - { - "startDate": "2023-07-29T02:30:51Z", - "quantity": 232 - }, - { - "startDate": "2023-07-29T02:35:51Z", - "quantity": 235 - }, - { - "startDate": "2023-07-29T02:40:52Z", - "quantity": 244 - }, - { - "startDate": "2023-07-29T02:45:51Z", - "quantity": 247 - }, - { - "startDate": "2023-07-29T02:50:52Z", - "quantity": 249 - }, - { - "startDate": "2023-07-29T02:55:52Z", - "quantity": 251 - }, - { - "startDate": "2023-07-29T03:00:52Z", - "quantity": 250 - }, - { - "startDate": "2023-07-29T03:05:52Z", - "quantity": 253 - }, - { - "startDate": "2023-07-29T03:10:52Z", - "quantity": 253 - }, - { - "startDate": "2023-07-29T03:15:52Z", - "quantity": 253 - }, - { - "startDate": "2023-07-29T03:20:52Z", - "quantity": 248 - }, - { - "startDate": "2023-07-29T03:25:51Z", - "quantity": 254 - }, - { - "startDate": "2023-07-29T03:30:52Z", - "quantity": 253 - }, - { - "startDate": "2023-07-29T03:35:51Z", - "quantity": 244 - }, - { - "startDate": "2023-07-29T03:40:52Z", - "quantity": 239 - }, - { - "startDate": "2023-07-29T03:45:51Z", - "quantity": 234 - }, - { - "startDate": "2023-07-29T03:50:52Z", - "quantity": 225 - }, - { - "startDate": "2023-07-29T03:55:52Z", - "quantity": 220 - }, - { - "startDate": "2023-07-29T04:00:51Z", - "quantity": 213 - }, - { - "startDate": "2023-07-29T04:05:52Z", - "quantity": 211 - }, - { - "startDate": "2023-07-29T04:10:52Z", - "quantity": 209 - }, - { - "startDate": "2023-07-29T04:15:52Z", - "quantity": 201 - }, - { - "startDate": "2023-07-29T04:20:52Z", - "quantity": 193 - }, - { - "startDate": "2023-07-29T04:25:52Z", - "quantity": 190 - }, - { - "startDate": "2023-07-29T04:30:51Z", - "quantity": 180 - }, - { - "startDate": "2023-07-29T04:35:51Z", - "quantity": 154 - }, - { - "startDate": "2023-07-29T04:40:52Z", - "quantity": 118 - }, - { - "startDate": "2023-07-29T04:45:52Z", - "quantity": 110 - }, - { - "startDate": "2023-07-29T04:50:51Z", - "quantity": 114 - }, - { - "startDate": "2023-07-29T04:55:51Z", - "quantity": 120 - }, - { - "startDate": "2023-07-29T05:00:51Z", - "quantity": 109 - }, - { - "startDate": "2023-07-29T05:05:51Z", - "quantity": 104 - }, - { - "startDate": "2023-07-29T05:10:51Z", - "quantity": 101 - }, - { - "startDate": "2023-07-29T05:15:52Z", - "quantity": 95 - }, - { - "startDate": "2023-07-29T05:20:52Z", - "quantity": 86 - }, - { - "startDate": "2023-07-29T05:25:51Z", - "quantity": 82 - }, - { - "startDate": "2023-07-29T05:30:52Z", - "quantity": 77 - }, - { - "startDate": "2023-07-29T05:35:52Z", - "quantity": 72 - }, - { - "startDate": "2023-07-29T05:40:51Z", - "quantity": 68 - }, - { - "startDate": "2023-07-29T05:45:52Z", - "quantity": 67 - }, - { - "startDate": "2023-07-29T05:50:52Z", - "quantity": 59 - }, - { - "startDate": "2023-07-29T05:55:52Z", - "quantity": 62 - }, - { - "startDate": "2023-07-29T06:00:52Z", - "quantity": 64 - }, - { - "startDate": "2023-07-29T06:05:52Z", - "quantity": 64 - }, - { - "startDate": "2023-07-29T06:10:52Z", - "quantity": 66 - }, - { - "startDate": "2023-07-29T06:15:52Z", - "quantity": 67 - }, - { - "startDate": "2023-07-29T06:20:52Z", - "quantity": 67 - }, - { - "startDate": "2023-07-29T06:25:52Z", - "quantity": 72 - }, - { - "startDate": "2023-07-29T06:30:52Z", - "quantity": 90 - }, - { - "startDate": "2023-07-29T06:35:52Z", - "quantity": 125 - }, - { - "startDate": "2023-07-29T06:40:52Z", - "quantity": 134 - }, - { - "startDate": "2023-07-29T06:45:52Z", - "quantity": 145 - }, - { - "startDate": "2023-07-29T06:50:52Z", - "quantity": 169 - }, - { - "startDate": "2023-07-29T06:55:52Z", - "quantity": 183 - }, - { - "startDate": "2023-07-29T07:00:52Z", - "quantity": 228 - }, - { - "startDate": "2023-07-29T07:05:53Z", - "quantity": 257 - }, - { - "startDate": "2023-07-29T07:10:52Z", - "quantity": 250 - }, - { - "startDate": "2023-07-29T07:15:52Z", - "quantity": 232 - }, - { - "startDate": "2023-07-29T07:20:52Z", - "quantity": 225 - }, - { - "startDate": "2023-07-29T07:25:52Z", - "quantity": 241 - }, - { - "startDate": "2023-07-29T07:30:52Z", - "quantity": 233 - }, - { - "startDate": "2023-07-29T07:35:53Z", - "quantity": 227 - }, - { - "startDate": "2023-07-29T07:40:52Z", - "quantity": 222 - }, - { - "startDate": "2023-07-29T07:45:52Z", - "quantity": 216 - }, - { - "startDate": "2023-07-29T07:50:52Z", - "quantity": 211 - }, - { - "startDate": "2023-07-29T07:55:52Z", - "quantity": 208 - }, - { - "startDate": "2023-07-29T08:00:52Z", - "quantity": 207 - }, - { - "startDate": "2023-07-29T08:05:52Z", - "quantity": 209 - }, - { - "startDate": "2023-07-29T08:10:52Z", - "quantity": 200 - }, - { - "startDate": "2023-07-29T08:15:52Z", - "quantity": 193 - }, - { - "startDate": "2023-07-29T08:20:52Z", - "quantity": 186 - }, - { - "startDate": "2023-07-29T08:25:52Z", - "quantity": 176 - }, - { - "startDate": "2023-07-29T08:30:52Z", - "quantity": 169 - }, - { - "startDate": "2023-07-29T08:35:52Z", - "quantity": 166 - }, - { - "startDate": "2023-07-29T08:40:52Z", - "quantity": 170 - }, - { - "startDate": "2023-07-29T08:45:52Z", - "quantity": 161 - }, - { - "startDate": "2023-07-29T08:50:53Z", - "quantity": 150 - }, - { - "startDate": "2023-07-29T08:55:53Z", - "quantity": 144 - }, - { - "startDate": "2023-07-29T09:00:53Z", - "quantity": 136 - }, - { - "startDate": "2023-07-29T09:05:53Z", - "quantity": 128 - }, - { - "startDate": "2023-07-29T09:10:52Z", - "quantity": 127 - }, - { - "startDate": "2023-07-29T09:15:53Z", - "quantity": 126 - }, - { - "startDate": "2023-07-29T09:20:53Z", - "quantity": 121 - }, - { - "startDate": "2023-07-29T09:25:52Z", - "quantity": 117 - }, - { - "startDate": "2023-07-29T09:30:52Z", - "quantity": 116 - }, - { - "startDate": "2023-07-29T09:35:53Z", - "quantity": 115 - }, - { - "startDate": "2023-07-29T09:40:52Z", - "quantity": 113 - }, - { - "startDate": "2023-07-29T09:45:52Z", - "quantity": 113 - }, - { - "startDate": "2023-07-29T09:50:52Z", - "quantity": 109 - }, - { - "startDate": "2023-07-29T09:55:53Z", - "quantity": 114 - }, - { - "startDate": "2023-07-29T10:00:52Z", - "quantity": 115 - }, - { - "startDate": "2023-07-29T10:05:52Z", - "quantity": 114 - }, - { - "startDate": "2023-07-29T10:10:53Z", - "quantity": 111 - }, - { - "startDate": "2023-07-29T10:15:52Z", - "quantity": 112 - }, - { - "startDate": "2023-07-29T10:20:52Z", - "quantity": 113 - }, - { - "startDate": "2023-07-29T10:25:53Z", - "quantity": 114 - }, - { - "startDate": "2023-07-29T10:30:52Z", - "quantity": 113 - }, - { - "startDate": "2023-07-29T10:35:52Z", - "quantity": 113 - }, - { - "startDate": "2023-07-29T10:40:53Z", - "quantity": 111 - }, - { - "startDate": "2023-07-29T10:45:53Z", - "quantity": 110 - }, - { - "startDate": "2023-07-29T10:50:53Z", - "quantity": 111 - }, - { - "startDate": "2023-07-29T10:55:52Z", - "quantity": 112 - }, - { - "startDate": "2023-07-29T11:00:53Z", - "quantity": 112 - }, - { - "startDate": "2023-07-29T11:05:52Z", - "quantity": 111 - }, - { - "startDate": "2023-07-29T11:10:52Z", - "quantity": 109 - }, - { - "startDate": "2023-07-29T11:15:52Z", - "quantity": 107 - }, - { - "startDate": "2023-07-29T11:20:52Z", - "quantity": 104 - }, - { - "startDate": "2023-07-29T11:25:53Z", - "quantity": 104 - }, - { - "startDate": "2023-07-29T11:30:53Z", - "quantity": 104 - }, - { - "startDate": "2023-07-29T11:35:53Z", - "quantity": 97 - }, - { - "startDate": "2023-07-29T11:40:53Z", - "quantity": 98 - }, - { - "startDate": "2023-07-29T11:45:52Z", - "quantity": 98 - }, - { - "startDate": "2023-07-29T11:50:53Z", - "quantity": 99 - }, - { - "startDate": "2023-07-29T11:55:52Z", - "quantity": 97 - }, - { - "startDate": "2023-07-29T12:00:53Z", - "quantity": 97 - }, - { - "startDate": "2023-07-29T12:05:53Z", - "quantity": 97 - }, - { - "startDate": "2023-07-29T12:10:53Z", - "quantity": 97 - }, - { - "startDate": "2023-07-29T12:15:52Z", - "quantity": 97 - }, - { - "startDate": "2023-07-29T12:20:53Z", - "quantity": 99 - }, - { - "startDate": "2023-07-29T12:25:53Z", - "quantity": 96 - }, - { - "startDate": "2023-07-29T12:30:53Z", - "quantity": 95 - }, - { - "startDate": "2023-07-29T12:35:53Z", - "quantity": 95 - }, - { - "startDate": "2023-07-29T12:40:53Z", - "quantity": 94 - }, - { - "startDate": "2023-07-29T12:45:53Z", - "quantity": 89 - }, - { - "startDate": "2023-07-29T12:50:53Z", - "quantity": 89 - }, - { - "startDate": "2023-07-29T12:55:53Z", - "quantity": 91 - }, - { - "startDate": "2023-07-29T13:00:52Z", - "quantity": 86 - }, - { - "startDate": "2023-07-29T13:05:53Z", - "quantity": 83 - }, - { - "startDate": "2023-07-29T13:10:53Z", - "quantity": 86 - }, - { - "startDate": "2023-07-29T13:15:53Z", - "quantity": 84 - }, - { - "startDate": "2023-07-29T13:20:53Z", - "quantity": 81 - }, - { - "startDate": "2023-07-29T13:25:53Z", - "quantity": 80 - }, - { - "startDate": "2023-07-29T13:30:53Z", - "quantity": 78 - }, - { - "startDate": "2023-07-29T13:35:53Z", - "quantity": 78 - }, - { - "startDate": "2023-07-29T13:40:53Z", - "quantity": 74 - }, - { - "startDate": "2023-07-29T13:45:53Z", - "quantity": 72 - }, - { - "startDate": "2023-07-29T13:50:52Z", - "quantity": 72 - }, - { - "startDate": "2023-07-29T13:55:53Z", - "quantity": 71 - }, - { - "startDate": "2023-07-29T14:00:53Z", - "quantity": 70 - }, - { - "startDate": "2023-07-29T14:05:53Z", - "quantity": 69 - }, - { - "startDate": "2023-07-29T14:10:53Z", - "quantity": 72 - }, - { - "startDate": "2023-07-29T14:15:53Z", - "quantity": 75 - }, - { - "startDate": "2023-07-29T14:20:53Z", - "quantity": 78 - }, - { - "startDate": "2023-07-29T14:25:53Z", - "quantity": 83 - }, - { - "startDate": "2023-07-29T14:30:53Z", - "quantity": 85 - }, - { - "startDate": "2023-07-29T14:35:53Z", - "quantity": 88 - }, - { - "startDate": "2023-07-29T14:40:53Z", - "quantity": 98 - }, - { - "startDate": "2023-07-29T14:45:53Z", - "quantity": 94 - }, - { - "startDate": "2023-07-29T14:50:53Z", - "quantity": 93 - }, - { - "startDate": "2023-07-29T14:55:53Z", - "quantity": 113 - }, - { - "startDate": "2023-07-29T15:00:53Z", - "quantity": 128 - }, - { - "startDate": "2023-07-29T15:05:53Z", - "quantity": 126 - }, - { - "startDate": "2023-07-29T15:10:54Z", - "quantity": 148 - }, - { - "startDate": "2023-07-29T15:15:54Z", - "quantity": 161 - }, - { - "startDate": "2023-07-29T15:20:54Z", - "quantity": 157 - }, - { - "startDate": "2023-07-29T15:25:53Z", - "quantity": 151 - }, - { - "startDate": "2023-07-29T15:30:53Z", - "quantity": 180 - }, - { - "startDate": "2023-07-29T15:35:53Z", - "quantity": 203 - }, - { - "startDate": "2023-07-29T15:40:54Z", - "quantity": 196 - }, - { - "startDate": "2023-07-29T15:45:54Z", - "quantity": 225 - }, - { - "startDate": "2023-07-29T15:50:53Z", - "quantity": 248 - }, - { - "startDate": "2023-07-29T15:55:53Z", - "quantity": 245 - }, - { - "startDate": "2023-07-29T16:00:53Z", - "quantity": 249 - }, - { - "startDate": "2023-07-29T16:05:53Z", - "quantity": 248 - }, - { - "startDate": "2023-07-29T16:10:54Z", - "quantity": 267 - }, - { - "startDate": "2023-07-29T16:15:53Z", - "quantity": 266 - }, - { - "startDate": "2023-07-29T16:20:54Z", - "quantity": 259 - }, - { - "startDate": "2023-07-29T16:25:54Z", - "quantity": 259 - }, - { - "startDate": "2023-07-29T16:30:53Z", - "quantity": 246 - }, - { - "startDate": "2023-07-29T16:35:54Z", - "quantity": 228 - }, - { - "startDate": "2023-07-29T16:40:53Z", - "quantity": 232 - }, - { - "startDate": "2023-07-29T16:45:53Z", - "quantity": 244 - }, - { - "startDate": "2023-07-29T16:50:53Z", - "quantity": 233 - }, - { - "startDate": "2023-07-29T16:55:53Z", - "quantity": 218 - }, - { - "startDate": "2023-07-29T17:00:53Z", - "quantity": 212 - }, - { - "startDate": "2023-07-29T17:05:53Z", - "quantity": 206 - }, - { - "startDate": "2023-07-29T17:10:53Z", - "quantity": 191 - }, - { - "startDate": "2023-07-29T17:15:54Z", - "quantity": 170 - }, - { - "startDate": "2023-07-29T17:20:54Z", - "quantity": 164 - }, - { - "startDate": "2023-07-29T17:25:53Z", - "quantity": 161 - }, - { - "startDate": "2023-07-29T17:30:53Z", - "quantity": 162 - }, - { - "startDate": "2023-07-29T17:35:53Z", - "quantity": 140 - }, - { - "startDate": "2023-07-29T17:40:54Z", - "quantity": 110 - }, - { - "startDate": "2023-07-29T17:45:54Z", - "quantity": 106 - }, - { - "startDate": "2023-07-29T17:50:53Z", - "quantity": 107 - }, - { - "startDate": "2023-07-29T17:55:53Z", - "quantity": 105 - }, - { - "startDate": "2023-07-29T18:00:53Z", - "quantity": 106 - }, - { - "startDate": "2023-07-29T18:05:54Z", - "quantity": 111 - }, - { - "startDate": "2023-07-29T18:10:53Z", - "quantity": 115 - }, - { - "startDate": "2023-07-29T18:15:53Z", - "quantity": 109 - }, - { - "startDate": "2023-07-29T18:20:54Z", - "quantity": 117 - }, - { - "startDate": "2023-07-29T18:25:54Z", - "quantity": 135 - }, - { - "startDate": "2023-07-29T18:30:53Z", - "quantity": 120 - }, - { - "startDate": "2023-07-29T18:35:53Z", - "quantity": 117 - }, - { - "startDate": "2023-07-29T18:40:53Z", - "quantity": 116 - }, - { - "startDate": "2023-07-29T18:45:53Z", - "quantity": 125 - }, - { - "startDate": "2023-07-29T18:50:53Z", - "quantity": 131 - }, - { - "startDate": "2023-07-29T18:55:53Z", - "quantity": 136 - }, - { - "startDate": "2023-07-29T19:00:53Z", - "quantity": 142 - }, - { - "startDate": "2023-07-29T19:05:53Z", - "quantity": 152 - }, - { - "startDate": "2023-07-29T19:10:54Z", - "quantity": 159 - }, - { - "startDate": "2023-07-29T19:15:54Z", - "quantity": 176 - }, - { - "startDate": "2023-07-29T19:20:54Z", - "quantity": 170 - } -] diff --git a/LoopTests/Managers/LoopAlgorithmTests.swift b/LoopTests/Managers/LoopAlgorithmTests.swift index 3f6476c8c8..92292a9569 100644 --- a/LoopTests/Managers/LoopAlgorithmTests.swift +++ b/LoopTests/Managers/LoopAlgorithmTests.swift @@ -70,8 +70,6 @@ final class LoopAlgorithmTests: XCTestCase { XCTAssertEqual(expected.startDate, calculated.startDate) XCTAssertEqual(expected.quantity.doubleValue(for: .milligramsPerDeciliter), calculated.quantity.doubleValue(for: .milligramsPerDeciliter), accuracy: defaultAccuracy) } - - //XCTAssertEqual(1.99, recommendedBasal!.unitsPerHour, accuracy: defaultAccuracy) } } From 687c055021d03d59e537b537ea977a0afe3d2803 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Tue, 5 Sep 2023 07:56:18 -0500 Subject: [PATCH 21/49] CarbValue struct update --- Loop/Extensions/DosingDecisionStore+SimulatedCoreData.swift | 2 +- Loop/Managers/LoopDataManager.swift | 2 +- LoopTests/Managers/LoopAlgorithmTests.swift | 1 - LoopTests/ViewModels/BolusEntryViewModelTests.swift | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Loop/Extensions/DosingDecisionStore+SimulatedCoreData.swift b/Loop/Extensions/DosingDecisionStore+SimulatedCoreData.swift index 558d634539..53f81c5209 100644 --- a/Loop/Extensions/DosingDecisionStore+SimulatedCoreData.swift +++ b/Loop/Extensions/DosingDecisionStore+SimulatedCoreData.swift @@ -147,7 +147,7 @@ fileprivate extension StoredDosingDecision { healthKitEligibleDate: nil) let carbsOnBoard = CarbValue(startDate: date, endDate: date.addingTimeInterval(.minutes(5)), - quantity: HKQuantity(unit: .gram(), doubleValue: 45.5)) + value: 45.5) let insulinOnBoard = InsulinValue(startDate: date, value: 1.5) let glucoseTargetRangeSchedule = GlucoseRangeSchedule(rangeSchedule: DailyQuantitySchedule(unit: .milligramsPerDeciliter, dailyItems: [RepeatingScheduleValue(startTime: .hours(0), value: DoubleRange(minValue: 100.0, maxValue: 110.0)), diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index 73b7e79abb..7927e5982e 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -1101,7 +1101,7 @@ extension LoopDataManager { switch error { case .noData: // when there is no data, carbs on board is set to 0 - self.carbsOnBoard = CarbValue(startDate: Date(), quantity: HKQuantity(unit: .gram(), doubleValue: 0)) + self.carbsOnBoard = CarbValue(startDate: Date(), value: 0) default: self.carbsOnBoard = nil warnings.append(.fetchDataWarning(.carbsOnBoard(error: error))) diff --git a/LoopTests/Managers/LoopAlgorithmTests.swift b/LoopTests/Managers/LoopAlgorithmTests.swift index 92292a9569..89831b4326 100644 --- a/LoopTests/Managers/LoopAlgorithmTests.swift +++ b/LoopTests/Managers/LoopAlgorithmTests.swift @@ -71,5 +71,4 @@ final class LoopAlgorithmTests: XCTestCase { XCTAssertEqual(expected.quantity.doubleValue(for: .milligramsPerDeciliter), calculated.quantity.doubleValue(for: .milligramsPerDeciliter), accuracy: defaultAccuracy) } } - } diff --git a/LoopTests/ViewModels/BolusEntryViewModelTests.swift b/LoopTests/ViewModels/BolusEntryViewModelTests.swift index 2877cb257b..c373b639b1 100644 --- a/LoopTests/ViewModels/BolusEntryViewModelTests.swift +++ b/LoopTests/ViewModels/BolusEntryViewModelTests.swift @@ -283,7 +283,7 @@ class BolusEntryViewModelTests: XCTestCase { } func testUpdateCarbsOnBoard() async throws { - delegate.carbsOnBoardResult = .success(CarbValue(startDate: Self.exampleStartDate, endDate: Self.exampleEndDate, quantity: Self.exampleCarbQuantity)) + delegate.carbsOnBoardResult = .success(CarbValue(startDate: Self.exampleStartDate, endDate: Self.exampleEndDate, value: Self.exampleCarbQuantity.doubleValue(for: .gram()))) XCTAssertNil(bolusEntryViewModel.activeCarbs) await bolusEntryViewModel.update() XCTAssertEqual(Self.exampleCarbQuantity, bolusEntryViewModel.activeCarbs) From d190fd5def92d56e707743c199fecf154d40ad34 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Wed, 6 Sep 2023 10:59:08 -0500 Subject: [PATCH 22/49] Update live capture test with updated durations of input data, and streamline app startup when running app just for tests --- Loop/AppDelegate.swift | 12 +- Loop/Managers/LoopDataManager.swift | 4 +- Loop/Models/LoopSettings+Loop.swift | 5 +- .../live_capture/live_capture_input.json | 1465 ++++++----------- .../live_capture_predicted_glucose.json | 314 ++-- LoopTests/Managers/LoopAlgorithmTests.swift | 3 +- LoopTests/Mock Stores/MockCarbStore.swift | 4 +- 7 files changed, 674 insertions(+), 1133 deletions(-) diff --git a/Loop/AppDelegate.swift b/Loop/AppDelegate.swift index 5da6ce9cb6..41569632d3 100644 --- a/Loop/AppDelegate.swift +++ b/Loop/AppDelegate.swift @@ -22,8 +22,11 @@ final class AppDelegate: UIResponder, UIApplicationDelegate, WindowProvider { setenv("CFNETWORK_DIAGNOSTICS", "3", 1) - loopAppManager.initialize(windowProvider: self, launchOptions: launchOptions) - loopAppManager.launch() + if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] == nil { + // Code only executes when not running tests + loopAppManager.initialize(windowProvider: self, launchOptions: launchOptions) + loopAppManager.launch() + } return loopAppManager.isLaunchComplete } @@ -32,7 +35,10 @@ final class AppDelegate: UIResponder, UIApplicationDelegate, WindowProvider { func applicationDidBecomeActive(_ application: UIApplication) { log.default(#function) - loopAppManager.didBecomeActive() + if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] == nil { + // Code only executes when not running tests + loopAppManager.didBecomeActive() + } } func applicationWillResignActive(_ application: UIApplication) { diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index 7927e5982e..e532b42e54 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -441,9 +441,9 @@ final class LoopDataManager { if lastIntegralRetrospectiveCorrectionEnabled != currentIntegralRetrospectiveCorrectionEnabled || cachedRetrospectiveCorrection == nil { lastIntegralRetrospectiveCorrectionEnabled = currentIntegralRetrospectiveCorrectionEnabled if currentIntegralRetrospectiveCorrectionEnabled { - cachedRetrospectiveCorrection = IntegralRetrospectiveCorrection(effectDuration: LoopSettings.retrospectiveCorrectionEffectDuration) + cachedRetrospectiveCorrection = IntegralRetrospectiveCorrection(effectDuration: LoopMath.retrospectiveCorrectionEffectDuration) } else { - cachedRetrospectiveCorrection = StandardRetrospectiveCorrection(effectDuration: LoopSettings.retrospectiveCorrectionEffectDuration) + cachedRetrospectiveCorrection = StandardRetrospectiveCorrection(effectDuration: LoopMath.retrospectiveCorrectionEffectDuration) } } diff --git a/Loop/Models/LoopSettings+Loop.swift b/Loop/Models/LoopSettings+Loop.swift index fd35b6416b..e4952934cb 100644 --- a/Loop/Models/LoopSettings+Loop.swift +++ b/Loop/Models/LoopSettings+Loop.swift @@ -16,8 +16,5 @@ extension LoopSettings { inputs.remove(.retrospection) } return inputs - } - - static let retrospectiveCorrectionEffectDuration = TimeInterval(hours: 1) - + } } diff --git a/LoopTests/Fixtures/live_capture/live_capture_input.json b/LoopTests/Fixtures/live_capture/live_capture_input.json index 95c27a166b..f010194a63 100644 --- a/LoopTests/Fixtures/live_capture/live_capture_input.json +++ b/LoopTests/Fixtures/live_capture/live_capture_input.json @@ -2,1476 +2,1003 @@ "carbEntries" : [ { "absorptionTime" : 10800, - "quantity" : 20, - "startDate" : "2023-08-17T20:05:47Z" + "quantity" : 22, + "startDate" : "2023-06-22T19:20:53Z" }, { "absorptionTime" : 10800, - "quantity" : 10, - "startDate" : "2023-08-18T18:01:14Z" + "quantity" : 75, + "startDate" : "2023-06-22T21:04:45Z" + }, + { + "absorptionTime" : 10800, + "quantity" : 47, + "startDate" : "2023-06-23T02:10:13Z" } ], "doses" : [ { - "endDate" : "2023-08-17T18:21:55Z", - "startDate" : "2023-08-17T18:01:54Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0 - }, - { - "endDate" : "2023-08-17T18:42:08Z", - "startDate" : "2023-08-17T18:21:55Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0 - }, - { - "endDate" : "2023-08-17T18:47:09Z", - "startDate" : "2023-08-17T18:42:08Z", + "endDate" : "2023-06-22T16:22:40Z", + "startDate" : "2023-06-22T16:12:40Z", "type" : "basal", "unit" : "U", - "value" : 0.1 + "value" : 0.050000000000000003 }, { - "endDate" : "2023-08-17T19:07:04Z", - "startDate" : "2023-08-17T18:47:09Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0 + "endDate" : "2023-06-22T16:17:54Z", + "startDate" : "2023-06-22T16:17:46Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.20000000000000001 }, { - "endDate" : "2023-08-17T19:11:53Z", - "startDate" : "2023-08-17T19:07:04Z", + "endDate" : "2023-06-22T16:32:40Z", + "startDate" : "2023-06-22T16:22:40Z", "type" : "tempBasal", "unit" : "U\/hour", "value" : 0 }, { - "endDate" : "2023-08-17T19:26:55Z", - "startDate" : "2023-08-17T19:11:53Z", + "endDate" : "2023-06-22T16:47:39Z", + "startDate" : "2023-06-22T16:32:40Z", "type" : "basal", "unit" : "U", - "value" : 0.3 - }, - { - "endDate" : "2023-08-17T19:47:02Z", - "startDate" : "2023-08-17T19:26:55Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0 - }, - { - "endDate" : "2023-08-17T20:06:54Z", - "startDate" : "2023-08-17T19:47:02Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0 + "value" : 0.10000000000000001 }, { - "endDate" : "2023-08-17T20:26:51Z", - "startDate" : "2023-08-17T20:06:54Z", + "endDate" : "2023-06-22T16:57:41Z", + "startDate" : "2023-06-22T16:47:39Z", "type" : "tempBasal", "unit" : "U\/hour", "value" : 0 }, { - "endDate" : "2023-08-17T20:46:53Z", - "startDate" : "2023-08-17T20:26:51Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0 - }, - { - "endDate" : "2023-08-17T21:06:52Z", - "startDate" : "2023-08-17T20:46:53Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0 - }, - { - "endDate" : "2023-08-17T21:46:54Z", - "startDate" : "2023-08-17T21:06:52Z", + "endDate" : "2023-06-22T17:02:38Z", + "startDate" : "2023-06-22T16:57:41Z", "type" : "basal", "unit" : "U", - "value" : 0.75 + "value" : 0.050000000000000003 }, { - "endDate" : "2023-08-17T21:51:52Z", - "startDate" : "2023-08-17T21:46:54Z", + "endDate" : "2023-06-22T17:07:38Z", + "startDate" : "2023-06-22T17:02:38Z", "type" : "tempBasal", "unit" : "U\/hour", - "value" : 0 + "value" : 0.050000000000000003 }, { - "endDate" : "2023-08-17T21:56:54Z", - "startDate" : "2023-08-17T21:51:52Z", + "endDate" : "2023-06-22T17:22:45Z", + "startDate" : "2023-06-22T17:07:38Z", "type" : "basal", "unit" : "U", - "value" : 0.1 + "value" : 0.10000000000000001 }, { - "endDate" : "2023-08-17T22:11:51Z", - "startDate" : "2023-08-17T21:56:54Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0 - }, - { - "endDate" : "2023-08-17T23:07:00Z", - "startDate" : "2023-08-17T22:11:51Z", - "type" : "basal", + "endDate" : "2023-06-22T17:12:46Z", + "startDate" : "2023-06-22T17:12:42Z", + "type" : "bolus", "unit" : "U", - "value" : 1 + "value" : 0.10000000000000001 }, { - "endDate" : "2023-08-17T23:11:53Z", - "startDate" : "2023-08-17T23:07:00Z", + "endDate" : "2023-06-22T17:27:39Z", + "startDate" : "2023-06-22T17:22:45Z", "type" : "tempBasal", "unit" : "U\/hour", "value" : 0 }, { - "endDate" : "2023-08-17T23:42:02Z", - "startDate" : "2023-08-17T23:11:53Z", + "endDate" : "2023-06-22T17:27:39Z", + "startDate" : "2023-06-22T17:27:39Z", "type" : "basal", "unit" : "U", - "value" : 0.55 - }, - { - "endDate" : "2023-08-18T00:02:07Z", - "startDate" : "2023-08-17T23:42:02Z", - "type" : "tempBasal", - "unit" : "U\/hour", "value" : 0 }, { - "endDate" : "2023-08-18T00:16:52Z", - "startDate" : "2023-08-18T00:02:07Z", + "endDate" : "2023-06-22T17:32:39Z", + "startDate" : "2023-06-22T17:27:39Z", "type" : "tempBasal", "unit" : "U\/hour", - "value" : 0 + "value" : 0.050000000000000003 }, { - "endDate" : "2023-08-18T01:26:52Z", - "startDate" : "2023-08-18T00:16:52Z", + "endDate" : "2023-06-22T18:07:38Z", + "startDate" : "2023-06-22T17:32:39Z", "type" : "basal", "unit" : "U", - "value" : 1.3 - }, - { - "endDate" : "2023-08-18T01:46:54Z", - "startDate" : "2023-08-18T01:26:52Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0 + "value" : 0.25 }, { - "endDate" : "2023-08-18T01:56:52Z", - "startDate" : "2023-08-18T01:46:54Z", - "type" : "basal", + "endDate" : "2023-06-22T17:32:45Z", + "startDate" : "2023-06-22T17:32:41Z", + "type" : "bolus", "unit" : "U", - "value" : 0.2 - }, - { - "endDate" : "2023-08-18T02:11:53Z", - "startDate" : "2023-08-18T01:56:52Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0 + "value" : 0.10000000000000001 }, { - "endDate" : "2023-08-18T02:41:53Z", - "startDate" : "2023-08-18T02:11:53Z", - "type" : "basal", + "endDate" : "2023-06-22T17:42:40Z", + "startDate" : "2023-06-22T17:42:38Z", + "type" : "bolus", "unit" : "U", - "value" : 0.55 - }, - { - "endDate" : "2023-08-18T02:47:02Z", - "startDate" : "2023-08-18T02:41:53Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0 + "value" : 0.050000000000000003 }, { - "endDate" : "2023-08-18T03:01:55Z", - "startDate" : "2023-08-18T02:47:02Z", - "type" : "basal", + "endDate" : "2023-06-22T17:47:43Z", + "startDate" : "2023-06-22T17:47:39Z", + "type" : "bolus", "unit" : "U", - "value" : 0.25 + "value" : 0.10000000000000001 }, { - "endDate" : "2023-08-18T03:06:52Z", - "startDate" : "2023-08-18T03:01:55Z", + "endDate" : "2023-06-22T18:12:38Z", + "startDate" : "2023-06-22T18:07:38Z", "type" : "tempBasal", "unit" : "U\/hour", "value" : 0 }, { - "endDate" : "2023-08-18T04:06:54Z", - "startDate" : "2023-08-18T03:06:52Z", + "endDate" : "2023-06-22T19:17:40Z", + "startDate" : "2023-06-22T18:12:38Z", "type" : "basal", "unit" : "U", - "value" : 1.1 + "value" : 0.45000000000000001 }, { - "endDate" : "2023-08-18T03:22:02Z", - "startDate" : "2023-08-18T03:21:50Z", + "endDate" : "2023-06-22T19:02:43Z", + "startDate" : "2023-06-22T19:02:39Z", "type" : "bolus", "unit" : "U", - "value" : 0.3 + "value" : 0.10000000000000001 + }, + { + "endDate" : "2023-06-22T19:22:43Z", + "startDate" : "2023-06-22T19:17:40Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 }, { - "endDate" : "2023-08-18T03:27:09Z", - "startDate" : "2023-08-18T03:27:05Z", + "endDate" : "2023-06-22T19:21:49Z", + "startDate" : "2023-06-22T19:21:01Z", "type" : "bolus", "unit" : "U", - "value" : 0.1 + "value" : 1.2 }, { - "endDate" : "2023-08-18T03:31:54Z", - "startDate" : "2023-08-18T03:31:50Z", - "type" : "bolus", + "endDate" : "2023-06-22T19:37:37Z", + "startDate" : "2023-06-22T19:22:43Z", + "type" : "basal", "unit" : "U", - "value" : 0.1 + "value" : 0.10000000000000001 }, { - "endDate" : "2023-08-18T03:41:53Z", - "startDate" : "2023-08-18T03:41:49Z", + "endDate" : "2023-06-22T19:27:43Z", + "startDate" : "2023-06-22T19:27:39Z", "type" : "bolus", "unit" : "U", - "value" : 0.1 + "value" : 0.10000000000000001 }, { - "endDate" : "2023-08-18T04:11:52Z", - "startDate" : "2023-08-18T04:06:54Z", + "endDate" : "2023-06-22T19:57:48Z", + "startDate" : "2023-06-22T19:37:37Z", "type" : "tempBasal", "unit" : "U\/hour", - "value" : 0.75 + "value" : 0 }, { - "endDate" : "2023-08-18T04:21:54Z", - "startDate" : "2023-08-18T04:11:52Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0.65 + "endDate" : "2023-06-22T19:57:48Z", + "startDate" : "2023-06-22T19:57:48Z", + "type" : "basal", + "unit" : "U", + "value" : 0 }, { - "endDate" : "2023-08-18T04:26:54Z", - "startDate" : "2023-08-18T04:21:54Z", + "endDate" : "2023-06-22T20:02:39Z", + "startDate" : "2023-06-22T19:57:48Z", "type" : "tempBasal", "unit" : "U\/hour", - "value" : 0.45 + "value" : 0 }, { - "endDate" : "2023-08-18T04:41:56Z", - "startDate" : "2023-08-18T04:26:54Z", + "endDate" : "2023-06-22T20:07:40Z", + "startDate" : "2023-06-22T20:02:39Z", "type" : "basal", "unit" : "U", - "value" : 0.3 + "value" : 0.050000000000000003 }, { - "endDate" : "2023-08-18T04:46:57Z", - "startDate" : "2023-08-18T04:41:56Z", + "endDate" : "2023-06-22T20:12:40Z", + "startDate" : "2023-06-22T20:07:40Z", "type" : "tempBasal", "unit" : "U\/hour", - "value" : 0.4 + "value" : 0.10000000000000001 }, { - "endDate" : "2023-08-18T04:51:52Z", - "startDate" : "2023-08-18T04:46:57Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0.1 + "endDate" : "2023-06-22T20:52:45Z", + "startDate" : "2023-06-22T20:12:40Z", + "type" : "basal", + "unit" : "U", + "value" : 0.25 }, { - "endDate" : "2023-08-18T05:00:00Z", - "startDate" : "2023-08-18T04:51:52Z", + "endDate" : "2023-06-22T21:07:43Z", + "startDate" : "2023-06-22T20:52:45Z", "type" : "tempBasal", "unit" : "U\/hour", "value" : 0 }, { - "endDate" : "2023-08-18T05:01:54Z", - "startDate" : "2023-08-18T05:00:00Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0 + "endDate" : "2023-06-22T21:07:49Z", + "startDate" : "2023-06-22T21:04:51Z", + "type" : "bolus", + "unit" : "U", + "value" : 4.4500000000000002 }, { - "endDate" : "2023-08-18T05:11:52Z", - "startDate" : "2023-08-18T05:01:54Z", + "endDate" : "2023-06-22T21:47:38Z", + "startDate" : "2023-06-22T21:07:43Z", "type" : "basal", "unit" : "U", - "value" : 0.2 - }, - { - "endDate" : "2023-08-18T05:16:57Z", - "startDate" : "2023-08-18T05:11:52Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0 + "value" : 0.25 }, { - "endDate" : "2023-08-18T05:27:01Z", - "startDate" : "2023-08-18T05:16:57Z", - "type" : "basal", + "endDate" : "2023-06-22T21:12:42Z", + "startDate" : "2023-06-22T21:12:40Z", + "type" : "bolus", "unit" : "U", - "value" : 0.2 + "value" : 0.050000000000000003 }, { - "endDate" : "2023-08-18T05:46:52Z", - "startDate" : "2023-08-18T05:27:01Z", + "endDate" : "2023-06-22T22:07:39Z", + "startDate" : "2023-06-22T21:47:38Z", "type" : "tempBasal", "unit" : "U\/hour", "value" : 0 }, { - "endDate" : "2023-08-18T06:01:53Z", - "startDate" : "2023-08-18T05:46:52Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0 + "endDate" : "2023-06-22T23:42:40Z", + "startDate" : "2023-06-22T22:07:39Z", + "type" : "basal", + "unit" : "U", + "value" : 0.65000000000000002 }, { - "endDate" : "2023-08-18T06:06:56Z", - "startDate" : "2023-08-18T06:01:53Z", - "type" : "basal", + "endDate" : "2023-06-22T22:27:46Z", + "startDate" : "2023-06-22T22:27:38Z", + "type" : "bolus", "unit" : "U", - "value" : 0.1 + "value" : 0.20000000000000001 }, { - "endDate" : "2023-08-18T06:22:05Z", - "startDate" : "2023-08-18T06:06:56Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0 + "endDate" : "2023-06-22T22:37:44Z", + "startDate" : "2023-06-22T22:37:40Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.10000000000000001 }, { - "endDate" : "2023-08-18T07:16:55Z", - "startDate" : "2023-08-18T06:22:05Z", - "type" : "basal", + "endDate" : "2023-06-22T22:42:42Z", + "startDate" : "2023-06-22T22:42:40Z", + "type" : "bolus", "unit" : "U", - "value" : 1 + "value" : 0.050000000000000003 }, { - "endDate" : "2023-08-18T07:26:58Z", - "startDate" : "2023-08-18T07:16:55Z", + "endDate" : "2023-06-22T23:52:44Z", + "startDate" : "2023-06-22T23:42:40Z", "type" : "tempBasal", "unit" : "U\/hour", "value" : 0 }, { - "endDate" : "2023-08-18T08:36:59Z", - "startDate" : "2023-08-18T07:26:58Z", + "endDate" : "2023-06-22T23:57:46Z", + "startDate" : "2023-06-22T23:52:44Z", "type" : "basal", "unit" : "U", - "value" : 1.3 + "value" : 0.050000000000000003 }, { - "endDate" : "2023-08-18T08:41:52Z", - "startDate" : "2023-08-18T08:36:59Z", + "endDate" : "2023-06-23T00:02:37Z", + "startDate" : "2023-06-22T23:57:46Z", "type" : "tempBasal", "unit" : "U\/hour", - "value" : 0 + "value" : 0.050000000000000003 }, { - "endDate" : "2023-08-18T08:51:52Z", - "startDate" : "2023-08-18T08:41:52Z", + "endDate" : "2023-06-23T01:02:52Z", + "startDate" : "2023-06-23T00:02:37Z", "type" : "basal", "unit" : "U", - "value" : 0.2 - }, - { - "endDate" : "2023-08-18T08:56:58Z", - "startDate" : "2023-08-18T08:51:52Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0 + "value" : 0.40000000000000002 }, { - "endDate" : "2023-08-18T09:12:01Z", - "startDate" : "2023-08-18T08:56:58Z", - "type" : "basal", + "endDate" : "2023-06-23T00:07:42Z", + "startDate" : "2023-06-23T00:07:40Z", + "type" : "bolus", "unit" : "U", - "value" : 0.3 + "value" : 0.050000000000000003 }, { - "endDate" : "2023-08-18T09:42:01Z", - "startDate" : "2023-08-18T09:12:01Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0 + "endDate" : "2023-06-23T00:12:44Z", + "startDate" : "2023-06-23T00:12:38Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.14999999999999999 }, { - "endDate" : "2023-08-18T09:51:54Z", - "startDate" : "2023-08-18T09:42:01Z", - "type" : "basal", + "endDate" : "2023-06-23T00:22:43Z", + "startDate" : "2023-06-23T00:22:39Z", + "type" : "bolus", "unit" : "U", - "value" : 0.2 + "value" : 0.10000000000000001 }, { - "endDate" : "2023-08-18T09:56:52Z", - "startDate" : "2023-08-18T09:51:54Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0 + "endDate" : "2023-06-23T00:27:49Z", + "startDate" : "2023-06-23T00:27:41Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.20000000000000001 }, { - "endDate" : "2023-08-18T10:06:56Z", - "startDate" : "2023-08-18T09:56:52Z", - "type" : "basal", + "endDate" : "2023-06-23T00:32:43Z", + "startDate" : "2023-06-23T00:32:39Z", + "type" : "bolus", "unit" : "U", - "value" : 0.2 + "value" : 0.10000000000000001 }, { - "endDate" : "2023-08-18T10:11:54Z", - "startDate" : "2023-08-18T10:06:56Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0 + "endDate" : "2023-06-23T00:37:58Z", + "startDate" : "2023-06-23T00:37:48Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.25 }, { - "endDate" : "2023-08-18T10:37:02Z", - "startDate" : "2023-08-18T10:11:54Z", - "type" : "basal", + "endDate" : "2023-06-23T00:42:47Z", + "startDate" : "2023-06-23T00:42:39Z", + "type" : "bolus", "unit" : "U", - "value" : 0.45 + "value" : 0.20000000000000001 }, { - "endDate" : "2023-08-18T10:41:54Z", - "startDate" : "2023-08-18T10:37:02Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0 + "endDate" : "2023-06-23T00:47:44Z", + "startDate" : "2023-06-23T00:47:40Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.10000000000000001 }, { - "endDate" : "2023-08-18T11:36:58Z", - "startDate" : "2023-08-18T10:41:54Z", - "type" : "basal", + "endDate" : "2023-06-23T00:52:51Z", + "startDate" : "2023-06-23T00:52:45Z", + "type" : "bolus", "unit" : "U", - "value" : 1 + "value" : 0.14999999999999999 }, { - "endDate" : "2023-08-18T11:51:54Z", - "startDate" : "2023-08-18T11:36:58Z", + "endDate" : "2023-06-23T01:12:49Z", + "startDate" : "2023-06-23T01:02:52Z", "type" : "tempBasal", "unit" : "U\/hour", "value" : 0 }, { - "endDate" : "2023-08-18T12:41:54Z", - "startDate" : "2023-08-18T11:51:54Z", + "endDate" : "2023-06-23T01:17:41Z", + "startDate" : "2023-06-23T01:12:49Z", "type" : "basal", "unit" : "U", - "value" : 0.9 + "value" : 0.050000000000000003 }, { - "endDate" : "2023-08-18T12:57:00Z", - "startDate" : "2023-08-18T12:41:54Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0 - }, - { - "endDate" : "2023-08-18T14:11:45Z", - "startDate" : "2023-08-18T12:57:00Z", - "type" : "basal", + "endDate" : "2023-06-23T01:12:54Z", + "startDate" : "2023-06-23T01:12:50Z", + "type" : "bolus", "unit" : "U", - "value" : 1.35 + "value" : 0.10000000000000001 }, { - "endDate" : "2023-08-18T14:17:00Z", - "startDate" : "2023-08-18T14:11:45Z", + "endDate" : "2023-06-23T01:37:39Z", + "startDate" : "2023-06-23T01:17:41Z", "type" : "tempBasal", "unit" : "U\/hour", "value" : 0 }, { - "endDate" : "2023-08-18T14:56:55Z", - "startDate" : "2023-08-18T14:17:00Z", + "endDate" : "2023-06-23T01:37:39Z", + "startDate" : "2023-06-23T01:37:39Z", "type" : "basal", "unit" : "U", - "value" : 0.75 + "value" : 0 }, { - "endDate" : "2023-08-18T15:17:08Z", - "startDate" : "2023-08-18T14:56:55Z", + "endDate" : "2023-06-23T01:42:38Z", + "startDate" : "2023-06-23T01:37:39Z", "type" : "tempBasal", "unit" : "U\/hour", "value" : 0 }, { - "endDate" : "2023-08-18T17:22:08Z", - "startDate" : "2023-08-18T15:17:08Z", + "endDate" : "2023-06-23T02:07:42Z", + "startDate" : "2023-06-23T01:42:38Z", "type" : "basal", "unit" : "U", - "value" : 2.3 + "value" : 0.14999999999999999 }, { - "endDate" : "2023-08-18T16:12:07Z", - "startDate" : "2023-08-18T16:12:03Z", + "endDate" : "2023-06-23T01:47:46Z", + "startDate" : "2023-06-23T01:47:38Z", "type" : "bolus", "unit" : "U", - "value" : 0.1 + "value" : 0.20000000000000001 }, { - "endDate" : "2023-08-18T16:37:09Z", - "startDate" : "2023-08-18T16:37:05Z", + "endDate" : "2023-06-23T01:52:47Z", + "startDate" : "2023-06-23T01:52:39Z", "type" : "bolus", "unit" : "U", - "value" : 0.1 + "value" : 0.20000000000000001 }, { - "endDate" : "2023-08-18T16:42:09Z", - "startDate" : "2023-08-18T16:42:05Z", + "endDate" : "2023-06-23T01:57:50Z", + "startDate" : "2023-06-23T01:57:40Z", "type" : "bolus", "unit" : "U", - "value" : 0.1 + "value" : 0.25 }, { - "endDate" : "2023-08-18T16:47:14Z", - "startDate" : "2023-08-18T16:47:10Z", + "endDate" : "2023-06-23T02:02:49Z", + "startDate" : "2023-06-23T02:02:39Z", "type" : "bolus", "unit" : "U", - "value" : 0.1 + "value" : 0.25 }, { - "endDate" : "2023-08-18T17:22:10Z", - "startDate" : "2023-08-18T17:22:08Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0.7 + "endDate" : "2023-06-23T02:07:36Z", + "startDate" : "2023-06-23T02:04:30Z", + "type" : "bolus", + "unit" : "U", + "value" : 4.6500000000000004 }, { - "endDate" : "2023-08-18T17:27:08Z", - "startDate" : "2023-08-18T17:22:10Z", + "endDate" : "2023-06-23T02:27:44Z", + "startDate" : "2023-06-23T02:07:42Z", "type" : "tempBasal", "unit" : "U\/hour", - "value" : 0.75 + "value" : 0 }, { - "endDate" : "2023-08-18T17:57:10Z", - "startDate" : "2023-08-18T17:27:08Z", + "endDate" : "2023-06-23T02:27:44Z", + "startDate" : "2023-06-23T02:27:44Z", "type" : "basal", "unit" : "U", - "value" : 0.55 + "value" : 0 }, { - "endDate" : "2023-08-18T18:02:05Z", - "startDate" : "2023-08-18T17:57:10Z", + "endDate" : "2023-06-23T02:47:39Z", + "startDate" : "2023-06-23T02:27:44Z", "type" : "tempBasal", "unit" : "U\/hour", - "value" : 0.55 - }, - { - "endDate" : "2023-08-18T18:01:46Z", - "startDate" : "2023-08-18T18:01:30Z", - "type" : "bolus", - "unit" : "U", - "value" : 0.4 + "value" : 0 } ], "glucoseHistory" : [ { - "quantity" : 81, - "startDate" : "2023-08-18T00:16:23Z" - }, - { - "quantity" : 82, - "startDate" : "2023-08-18T00:21:25Z" - }, - { - "quantity" : 84, - "startDate" : "2023-08-18T00:26:30Z" - }, - { - "quantity" : 85, - "startDate" : "2023-08-18T00:31:24Z" - }, - { - "quantity" : 88, - "startDate" : "2023-08-18T00:36:23Z" - }, - { - "quantity" : 89, - "startDate" : "2023-08-18T00:41:22Z" - }, - { - "quantity" : 90, - "startDate" : "2023-08-18T00:46:22Z" - }, - { - "quantity" : 90, - "startDate" : "2023-08-18T00:51:23Z" + "quantity" : 120, + "startDate" : "2023-06-22T16:42:33Z" }, { - "quantity" : 91, - "startDate" : "2023-08-18T00:56:23Z" + "quantity" : 119, + "startDate" : "2023-06-22T16:47:33Z" }, { - "quantity" : 91, - "startDate" : "2023-08-18T01:01:24Z" + "quantity" : 120, + "startDate" : "2023-06-22T16:52:34Z" }, { - "quantity" : 89, - "startDate" : "2023-08-18T01:06:25Z" + "quantity" : 118, + "startDate" : "2023-06-22T16:57:34Z" }, { - "quantity" : 87, - "startDate" : "2023-08-18T01:11:22Z" + "quantity" : 115, + "startDate" : "2023-06-22T17:02:34Z" }, { - "quantity" : 87, - "startDate" : "2023-08-18T01:16:23Z" + "quantity" : 120, + "startDate" : "2023-06-22T17:07:34Z" }, { - "quantity" : 85, - "startDate" : "2023-08-18T01:21:23Z" + "quantity" : 121, + "startDate" : "2023-06-22T17:12:34Z" }, { - "quantity" : 78, - "startDate" : "2023-08-18T01:26:22Z" + "quantity" : 119, + "startDate" : "2023-06-22T17:17:34Z" }, { - "quantity" : 80, - "startDate" : "2023-08-18T01:31:24Z" + "quantity" : 116, + "startDate" : "2023-06-22T17:22:34Z" }, { - "quantity" : 79, - "startDate" : "2023-08-18T01:36:25Z" + "quantity" : 115, + "startDate" : "2023-06-22T17:27:34Z" }, { - "quantity" : 81, - "startDate" : "2023-08-18T01:41:22Z" + "quantity" : 124, + "startDate" : "2023-06-22T17:32:34Z" }, { - "quantity" : 82, - "startDate" : "2023-08-18T01:46:23Z" + "quantity" : 114, + "startDate" : "2023-06-22T17:37:34Z" }, { - "quantity" : 82, - "startDate" : "2023-08-18T01:51:23Z" + "quantity" : 124, + "startDate" : "2023-06-22T17:42:34Z" }, { - "quantity" : 81, - "startDate" : "2023-08-18T01:56:23Z" + "quantity" : 124, + "startDate" : "2023-06-22T17:47:33Z" }, { - "quantity" : 80, - "startDate" : "2023-08-18T02:01:25Z" + "quantity" : 124, + "startDate" : "2023-06-22T17:52:34Z" }, { - "quantity" : 79, - "startDate" : "2023-08-18T02:06:29Z" + "quantity" : 126, + "startDate" : "2023-06-22T17:57:33Z" }, { - "quantity" : 81, - "startDate" : "2023-08-18T02:11:23Z" + "quantity" : 125, + "startDate" : "2023-06-22T18:02:34Z" }, { - "quantity" : 84, - "startDate" : "2023-08-18T02:16:25Z" + "quantity" : 118, + "startDate" : "2023-06-22T18:07:34Z" }, { - "quantity" : 86, - "startDate" : "2023-08-18T02:21:27Z" + "quantity" : 122, + "startDate" : "2023-06-22T18:12:33Z" }, { - "quantity" : 87, - "startDate" : "2023-08-18T02:26:24Z" + "quantity" : 123, + "startDate" : "2023-06-22T18:17:34Z" }, { - "quantity" : 84, - "startDate" : "2023-08-18T02:31:26Z" + "quantity" : 123, + "startDate" : "2023-06-22T18:22:34Z" }, { - "quantity" : 84, - "startDate" : "2023-08-18T02:36:25Z" + "quantity" : 121, + "startDate" : "2023-06-22T18:27:34Z" }, { - "quantity" : 82, - "startDate" : "2023-08-18T02:41:23Z" + "quantity" : 118, + "startDate" : "2023-06-22T18:32:34Z" }, { - "quantity" : 82, - "startDate" : "2023-08-18T02:46:31Z" + "quantity" : 116, + "startDate" : "2023-06-22T18:37:34Z" }, { - "quantity" : 99, - "startDate" : "2023-08-18T02:51:29Z" + "quantity" : 118, + "startDate" : "2023-06-22T18:42:34Z" }, { - "quantity" : 83, - "startDate" : "2023-08-18T02:56:25Z" + "quantity" : 115, + "startDate" : "2023-06-22T18:47:34Z" }, { - "quantity" : 83, - "startDate" : "2023-08-18T03:01:27Z" + "quantity" : 117, + "startDate" : "2023-06-22T18:52:34Z" }, { - "quantity" : 87, - "startDate" : "2023-08-18T03:06:23Z" + "quantity" : 125, + "startDate" : "2023-06-22T18:57:34Z" }, { - "quantity" : 90, - "startDate" : "2023-08-18T03:11:25Z" + "quantity" : 122, + "startDate" : "2023-06-22T19:02:34Z" }, { - "quantity" : 94, - "startDate" : "2023-08-18T03:16:29Z" + "quantity" : 119, + "startDate" : "2023-06-22T19:07:34Z" }, { - "quantity" : 101, - "startDate" : "2023-08-18T03:21:22Z" + "quantity" : 120, + "startDate" : "2023-06-22T19:12:34Z" }, { - "quantity" : 103, - "startDate" : "2023-08-18T03:26:39Z" - }, - { - "quantity" : 104, - "startDate" : "2023-08-18T03:31:22Z" + "quantity" : 112, + "startDate" : "2023-06-22T19:17:34Z" }, { - "quantity" : 106, - "startDate" : "2023-08-18T03:36:22Z" + "quantity" : 111, + "startDate" : "2023-06-22T19:22:34Z" }, { - "quantity" : 108, - "startDate" : "2023-08-18T03:41:22Z" + "quantity" : 114, + "startDate" : "2023-06-22T19:27:34Z" }, { - "quantity" : 108, - "startDate" : "2023-08-18T03:46:25Z" + "quantity" : 117, + "startDate" : "2023-06-22T19:32:34Z" }, { - "quantity" : 109, - "startDate" : "2023-08-18T03:51:24Z" + "quantity" : 107, + "startDate" : "2023-06-22T19:37:34Z" }, { - "quantity" : 110, - "startDate" : "2023-08-18T03:56:25Z" + "quantity" : 113, + "startDate" : "2023-06-22T19:42:34Z" }, { - "quantity" : 110, - "startDate" : "2023-08-18T04:01:27Z" + "quantity" : 117, + "startDate" : "2023-06-22T19:47:34Z" }, { "quantity" : 109, - "startDate" : "2023-08-18T04:06:24Z" - }, - { - "quantity" : 108, - "startDate" : "2023-08-18T04:11:23Z" - }, - { - "quantity" : 106, - "startDate" : "2023-08-18T04:16:23Z" - }, - { - "quantity" : 105, - "startDate" : "2023-08-18T04:21:24Z" - }, - { - "quantity" : 105, - "startDate" : "2023-08-18T04:26:23Z" - }, - { - "quantity" : 106, - "startDate" : "2023-08-18T04:31:26Z" - }, - { - "quantity" : 106, - "startDate" : "2023-08-18T04:36:24Z" - }, - { - "quantity" : 102, - "startDate" : "2023-08-18T04:41:25Z" - }, - { - "quantity" : 99, - "startDate" : "2023-08-18T04:46:27Z" - }, - { - "quantity" : 99, - "startDate" : "2023-08-18T04:51:22Z" - }, - { - "quantity" : 97, - "startDate" : "2023-08-18T04:56:24Z" - }, - { - "quantity" : 96, - "startDate" : "2023-08-18T05:01:23Z" - }, - { - "quantity" : 97, - "startDate" : "2023-08-18T05:06:24Z" - }, - { - "quantity" : 95, - "startDate" : "2023-08-18T05:11:22Z" - }, - { - "quantity" : 93, - "startDate" : "2023-08-18T05:16:26Z" - }, - { - "quantity" : 92, - "startDate" : "2023-08-18T05:21:22Z" - }, - { - "quantity" : 90, - "startDate" : "2023-08-18T05:26:33Z" - }, - { - "quantity" : 87, - "startDate" : "2023-08-18T05:31:22Z" - }, - { - "quantity" : 85, - "startDate" : "2023-08-18T05:36:23Z" - }, - { - "quantity" : 82, - "startDate" : "2023-08-18T05:41:36Z" - }, - { - "quantity" : 83, - "startDate" : "2023-08-18T05:46:22Z" - }, - { - "quantity" : 78, - "startDate" : "2023-08-18T05:51:23Z" - }, - { - "quantity" : 79, - "startDate" : "2023-08-18T05:56:25Z" - }, - { - "quantity" : 80, - "startDate" : "2023-08-18T06:01:24Z" - }, - { - "quantity" : 79, - "startDate" : "2023-08-18T06:06:26Z" - }, - { - "quantity" : 78, - "startDate" : "2023-08-18T06:11:29Z" - }, - { - "quantity" : 79, - "startDate" : "2023-08-18T06:16:24Z" - }, - { - "quantity" : 81, - "startDate" : "2023-08-18T06:21:35Z" - }, - { - "quantity" : 82, - "startDate" : "2023-08-18T06:26:31Z" - }, - { - "quantity" : 86, - "startDate" : "2023-08-18T06:31:25Z" - }, - { - "quantity" : 84, - "startDate" : "2023-08-18T06:36:30Z" - }, - { - "quantity" : 83, - "startDate" : "2023-08-18T06:41:22Z" - }, - { - "quantity" : 83, - "startDate" : "2023-08-18T06:46:22Z" - }, - { - "quantity" : 84, - "startDate" : "2023-08-18T06:51:31Z" - }, - { - "quantity" : 85, - "startDate" : "2023-08-18T06:56:35Z" - }, - { - "quantity" : 85, - "startDate" : "2023-08-18T07:01:25Z" - }, - { - "quantity" : 84, - "startDate" : "2023-08-18T07:06:29Z" - }, - { - "quantity" : 83, - "startDate" : "2023-08-18T07:11:22Z" - }, - { - "quantity" : 82, - "startDate" : "2023-08-18T07:16:24Z" - }, - { - "quantity" : 81, - "startDate" : "2023-08-18T07:21:31Z" - }, - { - "quantity" : 83, - "startDate" : "2023-08-18T07:26:27Z" - }, - { - "quantity" : 83, - "startDate" : "2023-08-18T07:31:22Z" - }, - { - "quantity" : 84, - "startDate" : "2023-08-18T07:36:24Z" - }, - { - "quantity" : 84, - "startDate" : "2023-08-18T07:41:23Z" + "startDate" : "2023-06-22T19:52:34Z" }, { - "quantity" : 85, - "startDate" : "2023-08-18T07:46:23Z" + "quantity" : 117, + "startDate" : "2023-06-22T19:57:34Z" }, { - "quantity" : 86, - "startDate" : "2023-08-18T07:51:25Z" + "quantity" : 121, + "startDate" : "2023-06-22T20:02:34Z" }, { - "quantity" : 89, - "startDate" : "2023-08-18T07:56:24Z" - }, - { - "quantity" : 90, - "startDate" : "2023-08-18T08:01:26Z" + "quantity" : 121, + "startDate" : "2023-06-22T20:07:34Z" }, { - "quantity" : 90, - "startDate" : "2023-08-18T08:06:33Z" + "quantity" : 127, + "startDate" : "2023-06-22T20:12:34Z" }, { - "quantity" : 91, - "startDate" : "2023-08-18T08:11:22Z" + "quantity" : 133, + "startDate" : "2023-06-22T20:17:34Z" }, { - "quantity" : 90, - "startDate" : "2023-08-18T08:16:23Z" + "quantity" : 131, + "startDate" : "2023-06-22T20:22:34Z" }, { - "quantity" : 89, - "startDate" : "2023-08-18T08:21:26Z" - }, - { - "quantity" : 89, - "startDate" : "2023-08-18T08:26:29Z" + "quantity" : 132, + "startDate" : "2023-06-22T20:27:34Z" }, { - "quantity" : 88, - "startDate" : "2023-08-18T08:31:25Z" + "quantity" : 134, + "startDate" : "2023-06-22T20:32:34Z" }, { - "quantity" : 87, - "startDate" : "2023-08-18T08:36:30Z" + "quantity" : 134, + "startDate" : "2023-06-22T20:37:34Z" }, { - "quantity" : 87, - "startDate" : "2023-08-18T08:41:22Z" + "quantity" : 139, + "startDate" : "2023-06-22T20:42:34Z" }, { - "quantity" : 87, - "startDate" : "2023-08-18T08:46:28Z" + "quantity" : 139, + "startDate" : "2023-06-22T20:47:34Z" }, { - "quantity" : 86, - "startDate" : "2023-08-18T08:51:23Z" + "quantity" : 132, + "startDate" : "2023-06-22T20:52:34Z" }, { - "quantity" : 86, - "startDate" : "2023-08-18T08:56:28Z" + "quantity" : 118, + "startDate" : "2023-06-22T20:57:34Z" }, { - "quantity" : 86, - "startDate" : "2023-08-18T09:01:26Z" + "quantity" : 123, + "startDate" : "2023-06-22T21:02:34Z" }, { - "quantity" : 86, - "startDate" : "2023-08-18T09:06:23Z" + "quantity" : 122, + "startDate" : "2023-06-22T21:07:34Z" }, { - "quantity" : 85, - "startDate" : "2023-08-18T09:11:30Z" + "quantity" : 119, + "startDate" : "2023-06-22T21:12:34Z" }, { - "quantity" : 85, - "startDate" : "2023-08-18T09:16:25Z" + "quantity" : 116, + "startDate" : "2023-06-22T21:17:34Z" }, { - "quantity" : 84, - "startDate" : "2023-08-18T09:21:25Z" + "quantity" : 113, + "startDate" : "2023-06-22T21:22:34Z" }, { - "quantity" : 84, - "startDate" : "2023-08-18T09:26:25Z" - }, - { - "quantity" : 83, - "startDate" : "2023-08-18T09:31:25Z" - }, - { - "quantity" : 83, - "startDate" : "2023-08-18T09:36:25Z" - }, - { - "quantity" : 82, - "startDate" : "2023-08-18T09:41:25Z" - }, - { - "quantity" : 82, - "startDate" : "2023-08-18T09:46:25Z" - }, - { - "quantity" : 81, - "startDate" : "2023-08-18T09:51:25Z" - }, - { - "quantity" : 82, - "startDate" : "2023-08-18T09:56:22Z" - }, - { - "quantity" : 81, - "startDate" : "2023-08-18T10:01:27Z" - }, - { - "quantity" : 81, - "startDate" : "2023-08-18T10:06:27Z" - }, - { - "quantity" : 81, - "startDate" : "2023-08-18T10:11:25Z" - }, - { - "quantity" : 82, - "startDate" : "2023-08-18T10:16:23Z" - }, - { - "quantity" : 83, - "startDate" : "2023-08-18T10:21:24Z" - }, - { - "quantity" : 83, - "startDate" : "2023-08-18T10:26:23Z" - }, - { - "quantity" : 82, - "startDate" : "2023-08-18T10:31:34Z" - }, - { - "quantity" : 81, - "startDate" : "2023-08-18T10:36:31Z" - }, - { - "quantity" : 81, - "startDate" : "2023-08-18T10:41:24Z" - }, - { - "quantity" : 82, - "startDate" : "2023-08-18T10:46:28Z" - }, - { - "quantity" : 81, - "startDate" : "2023-08-18T10:51:23Z" - }, - { - "quantity" : 82, - "startDate" : "2023-08-18T10:56:26Z" - }, - { - "quantity" : 84, - "startDate" : "2023-08-18T11:01:23Z" - }, - { - "quantity" : 84, - "startDate" : "2023-08-18T11:06:26Z" - }, - { - "quantity" : 85, - "startDate" : "2023-08-18T11:11:26Z" - }, - { - "quantity" : 87, - "startDate" : "2023-08-18T11:16:24Z" - }, - { - "quantity" : 88, - "startDate" : "2023-08-18T11:21:24Z" - }, - { - "quantity" : 88, - "startDate" : "2023-08-18T11:26:27Z" - }, - { - "quantity" : 84, - "startDate" : "2023-08-18T11:31:23Z" - }, - { - "quantity" : 82, - "startDate" : "2023-08-18T11:36:27Z" - }, - { - "quantity" : 81, - "startDate" : "2023-08-18T11:41:26Z" - }, - { - "quantity" : 81, - "startDate" : "2023-08-18T11:46:23Z" - }, - { - "quantity" : 84, - "startDate" : "2023-08-18T11:51:24Z" - }, - { - "quantity" : 84, - "startDate" : "2023-08-18T11:56:23Z" - }, - { - "quantity" : 86, - "startDate" : "2023-08-18T12:01:24Z" - }, - { - "quantity" : 86, - "startDate" : "2023-08-18T12:06:25Z" - }, - { - "quantity" : 86, - "startDate" : "2023-08-18T12:11:24Z" - }, - { - "quantity" : 86, - "startDate" : "2023-08-18T12:16:24Z" - }, - { - "quantity" : 86, - "startDate" : "2023-08-18T12:21:25Z" - }, - { - "quantity" : 87, - "startDate" : "2023-08-18T12:26:23Z" - }, - { - "quantity" : 87, - "startDate" : "2023-08-18T12:31:25Z" - }, - { - "quantity" : 88, - "startDate" : "2023-08-18T12:36:32Z" - }, - { - "quantity" : 81, - "startDate" : "2023-08-18T12:41:23Z" - }, - { - "quantity" : 78, - "startDate" : "2023-08-18T12:46:23Z" - }, - { - "quantity" : 77, - "startDate" : "2023-08-18T12:51:23Z" + "quantity" : 111, + "startDate" : "2023-06-22T21:27:34Z" }, { - "quantity" : 84, - "startDate" : "2023-08-18T12:56:28Z" + "quantity" : 112, + "startDate" : "2023-06-22T21:32:34Z" }, { - "quantity" : 88, - "startDate" : "2023-08-18T13:01:24Z" + "quantity" : 107, + "startDate" : "2023-06-22T21:37:34Z" }, { - "quantity" : 90, - "startDate" : "2023-08-18T13:06:23Z" + "quantity" : 102, + "startDate" : "2023-06-22T21:42:34Z" }, { - "quantity" : 85, - "startDate" : "2023-08-18T13:11:23Z" + "quantity" : 95, + "startDate" : "2023-06-22T21:47:34Z" }, { - "quantity" : 86, - "startDate" : "2023-08-18T13:16:23Z" + "quantity" : 96, + "startDate" : "2023-06-22T21:52:34Z" }, { "quantity" : 89, - "startDate" : "2023-08-18T13:21:24Z" + "startDate" : "2023-06-22T21:57:34Z" }, { - "quantity" : 86, - "startDate" : "2023-08-18T13:26:28Z" + "quantity" : 95, + "startDate" : "2023-06-22T22:02:34Z" }, { - "quantity" : 86, - "startDate" : "2023-08-18T13:31:24Z" + "quantity" : 95, + "startDate" : "2023-06-22T22:07:34Z" }, { - "quantity" : 87, - "startDate" : "2023-08-18T13:36:29Z" + "quantity" : 93, + "startDate" : "2023-06-22T22:12:34Z" }, { - "quantity" : 90, - "startDate" : "2023-08-18T13:41:23Z" + "quantity" : 98, + "startDate" : "2023-06-22T22:17:35Z" }, { - "quantity" : 89, - "startDate" : "2023-08-18T13:46:31Z" + "quantity" : 95, + "startDate" : "2023-06-22T22:22:35Z" }, { - "quantity" : 88, - "startDate" : "2023-08-18T13:51:29Z" + "quantity" : 101, + "startDate" : "2023-06-22T22:27:34Z" }, { - "quantity" : 92, - "startDate" : "2023-08-18T13:56:33Z" + "quantity" : 97, + "startDate" : "2023-06-22T22:32:34Z" }, { - "quantity" : 95, - "startDate" : "2023-08-18T14:01:37Z" + "quantity" : 108, + "startDate" : "2023-06-22T22:37:35Z" }, { - "quantity" : 88, - "startDate" : "2023-08-18T14:06:23Z" + "quantity" : 109, + "startDate" : "2023-06-22T22:42:34Z" }, { - "quantity" : 86, - "startDate" : "2023-08-18T14:11:26Z" + "quantity" : 109, + "startDate" : "2023-06-22T22:47:34Z" }, { - "quantity" : 92, - "startDate" : "2023-08-18T14:16:30Z" + "quantity" : 114, + "startDate" : "2023-06-22T22:52:34Z" }, { - "quantity" : 94, - "startDate" : "2023-08-18T14:21:29Z" + "quantity" : 115, + "startDate" : "2023-06-22T22:57:34Z" }, { - "quantity" : 96, - "startDate" : "2023-08-18T14:26:23Z" + "quantity" : 114, + "startDate" : "2023-06-22T23:02:34Z" }, { - "quantity" : 96, - "startDate" : "2023-08-18T14:31:23Z" + "quantity" : 121, + "startDate" : "2023-06-22T23:07:34Z" }, { - "quantity" : 98, - "startDate" : "2023-08-18T14:36:32Z" + "quantity" : 119, + "startDate" : "2023-06-22T23:12:34Z" }, { - "quantity" : 98, - "startDate" : "2023-08-18T14:41:25Z" + "quantity" : 117, + "startDate" : "2023-06-22T23:17:34Z" }, { - "quantity" : 99, - "startDate" : "2023-08-18T14:46:24Z" + "quantity" : 120, + "startDate" : "2023-06-22T23:22:35Z" }, { - "quantity" : 98, - "startDate" : "2023-08-18T14:51:26Z" + "quantity" : 122, + "startDate" : "2023-06-22T23:27:34Z" }, { - "quantity" : 94, - "startDate" : "2023-08-18T14:56:25Z" + "quantity" : 123, + "startDate" : "2023-06-22T23:32:34Z" }, { - "quantity" : 87, - "startDate" : "2023-08-18T15:01:24Z" + "quantity" : 127, + "startDate" : "2023-06-22T23:37:34Z" }, { - "quantity" : 85, - "startDate" : "2023-08-18T15:06:24Z" + "quantity" : 118, + "startDate" : "2023-06-22T23:42:35Z" }, { - "quantity" : 85, - "startDate" : "2023-08-18T15:11:23Z" + "quantity" : 120, + "startDate" : "2023-06-22T23:47:34Z" }, { - "quantity" : 86, - "startDate" : "2023-08-18T15:16:24Z" + "quantity" : 119, + "startDate" : "2023-06-22T23:52:35Z" }, { - "quantity" : 88, - "startDate" : "2023-08-18T15:21:23Z" + "quantity" : 115, + "startDate" : "2023-06-22T23:57:34Z" }, { - "quantity" : 89, - "startDate" : "2023-08-18T15:26:26Z" + "quantity" : 116, + "startDate" : "2023-06-23T00:02:34Z" }, { - "quantity" : 91, - "startDate" : "2023-08-18T15:31:24Z" + "quantity" : 133, + "startDate" : "2023-06-23T00:07:34Z" }, { - "quantity" : 92, - "startDate" : "2023-08-18T15:36:24Z" + "quantity" : 145, + "startDate" : "2023-06-23T00:12:34Z" }, { - "quantity" : 94, - "startDate" : "2023-08-18T15:41:25Z" + "quantity" : 140, + "startDate" : "2023-06-23T00:17:34Z" }, { - "quantity" : 96, - "startDate" : "2023-08-18T15:46:25Z" + "quantity" : 161, + "startDate" : "2023-06-23T00:22:35Z" }, { - "quantity" : 96, - "startDate" : "2023-08-18T15:51:28Z" + "quantity" : 166, + "startDate" : "2023-06-23T00:27:34Z" }, { - "quantity" : 98, - "startDate" : "2023-08-18T15:56:26Z" + "quantity" : 172, + "startDate" : "2023-06-23T00:32:35Z" }, { - "quantity" : 97, - "startDate" : "2023-08-18T16:01:23Z" + "quantity" : 182, + "startDate" : "2023-06-23T00:37:35Z" }, { - "quantity" : 99, - "startDate" : "2023-08-18T16:06:23Z" + "quantity" : 184, + "startDate" : "2023-06-23T00:42:35Z" }, { - "quantity" : 101, - "startDate" : "2023-08-18T16:11:25Z" + "quantity" : 185, + "startDate" : "2023-06-23T00:47:34Z" }, { - "quantity" : 102, - "startDate" : "2023-08-18T16:16:25Z" + "quantity" : 190, + "startDate" : "2023-06-23T00:52:35Z" }, { - "quantity" : 103, - "startDate" : "2023-08-18T16:21:23Z" + "quantity" : 182, + "startDate" : "2023-06-23T00:57:34Z" }, { - "quantity" : 98, - "startDate" : "2023-08-18T16:26:23Z" + "quantity" : 166, + "startDate" : "2023-06-23T01:02:35Z" }, { - "quantity" : 102, - "startDate" : "2023-08-18T16:31:28Z" + "quantity" : 174, + "startDate" : "2023-06-23T01:07:34Z" }, { - "quantity" : 105, - "startDate" : "2023-08-18T16:36:23Z" + "quantity" : 179, + "startDate" : "2023-06-23T01:12:34Z" }, { - "quantity" : 112, - "startDate" : "2023-08-18T16:41:23Z" + "quantity" : 166, + "startDate" : "2023-06-23T01:17:35Z" }, { - "quantity" : 112, - "startDate" : "2023-08-18T16:46:23Z" + "quantity" : 134, + "startDate" : "2023-06-23T01:22:34Z" }, { - "quantity" : 111, - "startDate" : "2023-08-18T16:51:26Z" + "quantity" : 131, + "startDate" : "2023-06-23T01:27:35Z" }, { - "quantity" : 111, - "startDate" : "2023-08-18T16:56:24Z" + "quantity" : 129, + "startDate" : "2023-06-23T01:32:34Z" }, { - "quantity" : 111, - "startDate" : "2023-08-18T17:01:23Z" + "quantity" : 136, + "startDate" : "2023-06-23T01:37:34Z" }, { - "quantity" : 109, - "startDate" : "2023-08-18T17:06:24Z" + "quantity" : 152, + "startDate" : "2023-06-23T01:42:34Z" }, { - "quantity" : 111, - "startDate" : "2023-08-18T17:11:23Z" + "quantity" : 162, + "startDate" : "2023-06-23T01:47:35Z" }, { - "quantity" : 111, - "startDate" : "2023-08-18T17:16:23Z" + "quantity" : 165, + "startDate" : "2023-06-23T01:52:34Z" }, { - "quantity" : 110, - "startDate" : "2023-08-18T17:21:23Z" + "quantity" : 172, + "startDate" : "2023-06-23T01:57:34Z" }, { - "quantity" : 111, - "startDate" : "2023-08-18T17:26:23Z" + "quantity" : 176, + "startDate" : "2023-06-23T02:02:35Z" }, { - "quantity" : 110, - "startDate" : "2023-08-18T17:31:25Z" + "quantity" : 165, + "startDate" : "2023-06-23T02:07:35Z" }, { - "quantity" : 110, - "startDate" : "2023-08-18T17:36:23Z" + "quantity" : 172, + "startDate" : "2023-06-23T02:12:34Z" }, { - "quantity" : 110, - "startDate" : "2023-08-18T17:41:26Z" + "quantity" : 170, + "startDate" : "2023-06-23T02:17:35Z" }, { - "quantity" : 110, - "startDate" : "2023-08-18T17:46:30Z" + "quantity" : 177, + "startDate" : "2023-06-23T02:22:35Z" }, { - "quantity" : 111, - "startDate" : "2023-08-18T17:51:28Z" + "quantity" : 176, + "startDate" : "2023-06-23T02:27:35Z" }, { - "quantity" : 106, - "startDate" : "2023-08-18T17:56:24Z" + "quantity" : 173, + "startDate" : "2023-06-23T02:32:34Z" }, { - "quantity" : 102, - "startDate" : "2023-08-18T18:01:24Z" + "quantity" : 180, + "startDate" : "2023-06-23T02:37:35Z" } ], "settings" : { "basal" : [ { - "endDate" : "2023-08-18T05:00:00Z", - "startDate" : "2023-08-17T05:00:00Z", - "value" : 1.1 - }, - { - "endDate" : "2023-08-19T05:00:00Z", - "startDate" : "2023-08-18T05:00:00Z", - "value" : 1.1 + "endDate" : "2023-06-23T05:00:00Z", + "startDate" : "2023-06-22T10:00:00Z", + "value" : 0.45000000000000001 } ], "carbRatio" : [ { - "endDate" : "2023-08-18T05:00:00Z", - "startDate" : "2023-08-17T05:00:00Z", - "value" : 15 - }, - { - "endDate" : "2023-08-19T05:00:00Z", - "startDate" : "2023-08-18T05:00:00Z", - "value" : 15 + "endDate" : "2023-06-23T07:00:00Z", + "startDate" : "2023-06-22T07:00:00Z", + "value" : 11 } ], + "maximumBasalRatePerHour" : null, + "maximumBolus" : null, "sensitivity" : [ { - "endDate" : "2023-08-18T05:00:00Z", - "startDate" : "2023-08-17T05:00:00Z", - "value" : 50 - }, - { - "endDate" : "2023-08-19T05:00:00Z", - "startDate" : "2023-08-18T05:00:00Z", - "value" : 50 + "endDate" : "2023-06-23T05:00:00Z", + "startDate" : "2023-06-22T10:00:00Z", + "value" : 60 } ], + "suspendThreshold" : null, "target" : [ { - "endDate" : "2023-08-19T00:15:00Z", - "startDate" : "2023-08-18T11:50:00Z", + "endDate" : "2023-06-23T07:00:00Z", + "startDate" : "2023-06-22T20:25:00Z", + "value" : { + "maxValue" : 115, + "minValue" : 100 + } + }, + { + "endDate" : "2023-06-23T08:50:00Z", + "startDate" : "2023-06-23T07:00:00Z", "value" : { "maxValue" : 115, "minValue" : 100 diff --git a/LoopTests/Fixtures/live_capture/live_capture_predicted_glucose.json b/LoopTests/Fixtures/live_capture/live_capture_predicted_glucose.json index bb3a141d73..a98fbaccb7 100644 --- a/LoopTests/Fixtures/live_capture/live_capture_predicted_glucose.json +++ b/LoopTests/Fixtures/live_capture/live_capture_predicted_glucose.json @@ -1,382 +1,392 @@ [ { - "quantity" : 102, + "quantity" : 180, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T18:01:24Z" + "startDate" : "2023-06-23T02:37:35Z" }, { - "quantity" : 99.896393318404151, + "quantity" : 180.29132150657966, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T18:05:00Z" + "startDate" : "2023-06-23T02:40:00Z" }, { - "quantity" : 97.327016523041976, + "quantity" : 180.51458820506667, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T18:10:00Z" + "startDate" : "2023-06-23T02:45:00Z" }, { - "quantity" : 95.324444788485806, + "quantity" : 179.7158986124237, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T18:15:00Z" + "startDate" : "2023-06-23T02:50:00Z" }, { - "quantity" : 93.992918445816656, + "quantity" : 177.66868460973922, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T18:20:00Z" + "startDate" : "2023-06-23T02:55:00Z" }, { - "quantity" : 92.98678930503138, + "quantity" : 174.80252509117634, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T18:25:00Z" + "startDate" : "2023-06-23T03:00:00Z" }, { - "quantity" : 92.153401829419991, + "quantity" : 171.74984493231631, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T18:30:00Z" + "startDate" : "2023-06-23T03:05:00Z" }, { - "quantity" : 91.502805972419452, + "quantity" : 168.58187755437024, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T18:35:00Z" + "startDate" : "2023-06-23T03:10:00Z" }, { - "quantity" : 91.044140230125493, + "quantity" : 165.36216340804185, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T18:40:00Z" + "startDate" : "2023-06-23T03:15:00Z" }, { - "quantity" : 90.785701374247225, + "quantity" : 162.12697210734922, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T18:45:00Z" + "startDate" : "2023-06-23T03:20:00Z" }, { - "quantity" : 90.735009333107385, + "quantity" : 158.90986429144345, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T18:50:00Z" + "startDate" : "2023-06-23T03:25:00Z" }, { - "quantity" : 90.87529571413188, + "quantity" : 155.75684851046043, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T18:55:00Z" + "startDate" : "2023-06-23T03:30:00Z" }, { - "quantity" : 91.131206561299791, + "quantity" : 152.70869296700107, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T19:00:00Z" + "startDate" : "2023-06-23T03:35:00Z" }, { - "quantity" : 91.369852449274902, + "quantity" : 149.78068888956841, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T19:05:00Z" + "startDate" : "2023-06-23T03:40:00Z" }, { - "quantity" : 91.596637899447671, + "quantity" : 147.00401242102828, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T19:10:00Z" + "startDate" : "2023-06-23T03:45:00Z" }, { - "quantity" : 91.815952582397927, + "quantity" : 144.40563853768242, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T19:15:00Z" + "startDate" : "2023-06-23T03:50:00Z" }, { - "quantity" : 92.031661009999482, + "quantity" : 142.0087170601098, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T19:20:00Z" + "startDate" : "2023-06-23T03:55:00Z" }, { - "quantity" : 92.24719829385748, + "quantity" : 139.83295658233396, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T19:25:00Z" + "startDate" : "2023-06-23T04:00:00Z" }, { - "quantity" : 92.465606922060431, + "quantity" : 137.89511837124121, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T19:30:00Z" + "startDate" : "2023-06-23T04:05:00Z" }, { - "quantity" : 92.689569938551358, + "quantity" : 136.07526338088792, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T19:35:00Z" + "startDate" : "2023-06-23T04:10:00Z" }, { - "quantity" : 92.921441679246016, + "quantity" : 134.25815754225141, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T19:40:00Z" + "startDate" : "2023-06-23T04:15:00Z" }, { - "quantity" : 93.163276230779218, + "quantity" : 132.45275084533137, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T19:45:00Z" + "startDate" : "2023-06-23T04:20:00Z" }, { - "quantity" : 93.416853767095574, + "quantity" : 130.66563522056958, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T19:50:00Z" + "startDate" : "2023-06-23T04:25:00Z" }, { - "quantity" : 93.683704909090238, + "quantity" : 128.90146920949769, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T19:55:00Z" + "startDate" : "2023-06-23T04:30:00Z" }, { - "quantity" : 93.965133243123091, + "quantity" : 127.16322092092855, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T20:00:00Z" + "startDate" : "2023-06-23T04:35:00Z" }, { - "quantity" : 94.262236125417473, + "quantity" : 125.45215396105368, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T20:05:00Z" + "startDate" : "2023-06-23T04:40:00Z" }, { - "quantity" : 94.575923891105845, + "quantity" : 123.76712483433676, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T20:10:00Z" + "startDate" : "2023-06-23T04:45:00Z" }, { - "quantity" : 94.906937578934446, + "quantity" : 122.10683165409341, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T20:15:00Z" + "startDate" : "2023-06-23T04:50:00Z" }, { - "quantity" : 95.255865275387578, + "quantity" : 120.46857875163471, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T20:20:00Z" + "startDate" : "2023-06-23T04:55:00Z" }, { - "quantity" : 95.623303645444025, + "quantity" : 118.84903308222181, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T20:25:00Z" + "startDate" : "2023-06-23T05:00:00Z" }, { - "quantity" : 96.000882373540378, + "quantity" : 117.24445077397047, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T20:30:00Z" + "startDate" : "2023-06-23T05:05:00Z" }, { - "quantity" : 96.365336203749791, + "quantity" : 115.65043839655846, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T20:35:00Z" + "startDate" : "2023-06-23T05:10:00Z" }, { - "quantity" : 96.715562621883805, + "quantity" : 114.06198688414838, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T20:40:00Z" + "startDate" : "2023-06-23T05:15:00Z" }, { - "quantity" : 97.051545329583519, + "quantity" : 112.47356001340279, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T20:45:00Z" + "startDate" : "2023-06-23T05:20:00Z" }, { - "quantity" : 97.373191168681046, + "quantity" : 110.87917488553444, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T20:50:00Z" + "startDate" : "2023-06-23T05:25:00Z" }, { - "quantity" : 97.68033912753441, + "quantity" : 109.27247502015473, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T20:55:00Z" + "startDate" : "2023-06-23T05:30:00Z" }, { - "quantity" : 97.972768583817071, + "quantity" : 107.64679662666447, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T21:00:00Z" + "startDate" : "2023-06-23T05:35:00Z" }, { - "quantity" : 98.250206839942507, + "quantity" : 105.99522857963143, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T21:05:00Z" + "startDate" : "2023-06-23T05:40:00Z" }, { - "quantity" : 98.512461657029007, + "quantity" : 104.31066658787131, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T21:10:00Z" + "startDate" : "2023-06-23T05:45:00Z" }, { - "quantity" : 98.759768446355963, + "quantity" : 102.58586201263279, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T21:15:00Z" + "startDate" : "2023-06-23T05:50:00Z" }, { - "quantity" : 98.992334190285419, + "quantity" : 100.81350120847731, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T21:20:00Z" + "startDate" : "2023-06-23T05:55:00Z" }, { - "quantity" : 99.210294956891744, + "quantity" : 98.986445102805988, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T21:25:00Z" + "startDate" : "2023-06-23T06:00:00Z" }, { - "quantity" : 99.413602382932424, + "quantity" : 97.097518927124952, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T21:30:00Z" + "startDate" : "2023-06-23T06:05:00Z" }, { - "quantity" : 99.601698280019434, + "quantity" : 95.139330662672023, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T21:35:00Z" + "startDate" : "2023-06-23T06:10:00Z" }, { - "quantity" : 99.773947375251453, + "quantity" : 93.104670202578632, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T21:40:00Z" + "startDate" : "2023-06-23T06:15:00Z" }, { - "quantity" : 99.929708236532747, + "quantity" : 90.986165185301502, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T21:45:00Z" + "startDate" : "2023-06-23T06:20:00Z" }, { - "quantity" : 100.06833834582778, + "quantity" : 88.909927040807588, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T21:50:00Z" + "startDate" : "2023-06-23T06:25:00Z" }, { - "quantity" : 100.18919669640144, + "quantity" : 86.994338611676767, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T21:55:00Z" + "startDate" : "2023-06-23T06:30:00Z" }, { - "quantity" : 100.29164609469677, + "quantity" : 85.232136877351081, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T22:00:00Z" + "startDate" : "2023-06-23T06:35:00Z" }, { - "quantity" : 100.3750551915391, + "quantity" : 83.615651290380811, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T22:05:00Z" + "startDate" : "2023-06-23T06:40:00Z" }, { - "quantity" : 100.43880026555911, + "quantity" : 82.136746744082188, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T22:10:00Z" + "startDate" : "2023-06-23T06:45:00Z" }, { - "quantity" : 100.48226678004278, + "quantity" : 80.787935960558002, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T22:15:00Z" + "startDate" : "2023-06-23T06:50:00Z" }, { - "quantity" : 100.50485073285557, + "quantity" : 79.561150334091622, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T22:20:00Z" + "startDate" : "2023-06-23T06:55:00Z" }, { - "quantity" : 100.50583426131806, + "quantity" : 78.448809315519384, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T22:25:00Z" + "startDate" : "2023-06-23T07:00:00Z" }, { - "quantity" : 100.48412374566237, + "quantity" : 77.444295000376087, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T22:30:00Z" + "startDate" : "2023-06-23T07:05:00Z" }, { - "quantity" : 100.4391400838507, + "quantity" : 76.541144021775267, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T22:35:00Z" + "startDate" : "2023-06-23T07:10:00Z" }, { - "quantity" : 100.3703782423454, + "quantity" : 75.734033247701291, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T22:40:00Z" + "startDate" : "2023-06-23T07:15:00Z" }, { - "quantity" : 100.28611412349363, + "quantity" : 75.018229944400559, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T22:45:00Z" + "startDate" : "2023-06-23T07:20:00Z" }, { - "quantity" : 100.20947967884376, + "quantity" : 74.389076912965834, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T22:50:00Z" + "startDate" : "2023-06-23T07:25:00Z" }, { - "quantity" : 100.14069735835136, + "quantity" : 73.841309919727451, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T22:55:00Z" + "startDate" : "2023-06-23T07:30:00Z" }, { - "quantity" : 100.07866920613634, + "quantity" : 73.370549918316215, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T23:00:00Z" + "startDate" : "2023-06-23T07:35:00Z" }, { - "quantity" : 100.02247162981837, + "quantity" : 72.972744055408953, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T23:05:00Z" + "startDate" : "2023-06-23T07:40:00Z" }, { - "quantity" : 99.971752267355782, + "quantity" : 72.643975082565134, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T23:10:00Z" + "startDate" : "2023-06-23T07:45:00Z" }, { - "quantity" : 99.926235939245828, + "quantity" : 72.380461060355856, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T23:15:00Z" + "startDate" : "2023-06-23T07:50:00Z" }, { - "quantity" : 99.88565546474166, + "quantity" : 72.178520063294286, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T23:20:00Z" + "startDate" : "2023-06-23T07:55:00Z" }, { - "quantity" : 99.849751805109122, + "quantity" : 72.034174053629386, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T23:25:00Z" + "startDate" : "2023-06-23T08:00:00Z" }, { - "quantity" : 99.81827416145336, + "quantity" : 71.942299096190823, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T23:30:00Z" + "startDate" : "2023-06-23T08:05:00Z" }, { - "quantity" : 99.791028333933468, + "quantity" : 71.897751011456421, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T23:35:00Z" + "startDate" : "2023-06-23T08:10:00Z" }, { - "quantity" : 99.767995772848735, + "quantity" : 71.895123880236383, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T23:40:00Z" + "startDate" : "2023-06-23T08:15:00Z" }, { - "quantity" : 99.748959024064902, + "quantity" : 71.906254842464136, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T23:45:00Z" + "startDate" : "2023-06-23T08:20:00Z" }, { - "quantity" : 99.733680926725469, + "quantity" : 71.914434937142801, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T23:50:00Z" + "startDate" : "2023-06-23T08:25:00Z" }, { - "quantity" : 99.721933417247726, + "quantity" : 71.920167940771535, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-18T23:55:00Z" + "startDate" : "2023-06-23T08:30:00Z" }, { - "quantity" : 99.713497415806842, + "quantity" : 71.923927819981145, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-19T00:00:00Z" + "startDate" : "2023-06-23T08:35:00Z" }, { - "quantity" : 99.708162692555675, + "quantity" : 71.926159114246957, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-19T00:05:00Z" + "startDate" : "2023-06-23T08:40:00Z" }, { - "quantity" : 99.705774231311352, + "quantity" : 71.927280081079402, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-19T00:10:00Z" + "startDate" : "2023-06-23T08:45:00Z" }, { - "quantity" : 99.705641639222222, + "quantity" : 71.927682355083221, "quantityUnit" : "mg\/dL", - "startDate" : "2023-08-19T00:15:00Z" + "startDate" : "2023-06-23T08:50:00Z" + }, + { + "quantity" : 71.927731342958282, + "quantityUnit" : "mg\/dL", + "startDate" : "2023-06-23T08:55:00Z" + }, + { + "quantity" : 71.927731342958282, + "quantityUnit" : "mg\/dL", + "startDate" : "2023-06-23T09:00:00Z" } ] diff --git a/LoopTests/Managers/LoopAlgorithmTests.swift b/LoopTests/Managers/LoopAlgorithmTests.swift index 89831b4326..6c51283872 100644 --- a/LoopTests/Managers/LoopAlgorithmTests.swift +++ b/LoopTests/Managers/LoopAlgorithmTests.swift @@ -51,7 +51,8 @@ final class LoopAlgorithmTests: XCTestCase { func testLiveCaptureWithFunctionalAlgorithm() throws { // This matches the "testForecastFromLiveCaptureInputData" test of LoopDataManagerDosingTests, - // Using the same input data, but generating the forecast using LoopPrediction + // Using the same input data, but generating the forecast using the LoopAlgorithm.generatePrediction() + // function. let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 diff --git a/LoopTests/Mock Stores/MockCarbStore.swift b/LoopTests/Mock Stores/MockCarbStore.swift index 2c8d7407e7..4a5c016eb5 100644 --- a/LoopTests/Mock Stores/MockCarbStore.swift +++ b/LoopTests/Mock Stores/MockCarbStore.swift @@ -108,8 +108,8 @@ class MockCarbStore: CarbStoreProtocol { let carbDates = samples.map { $0.startDate } let maxCarbDate = carbDates.max()! let minCarbDate = carbDates.min()! - let carbRatio = carbRatioScheduleApplyingOverrideHistory.between(start: maxCarbDate, end: minCarbDate) - let insulinSensitivity = insulinSensitivityScheduleApplyingOverrideHistory.quantitiesBetween(start: maxCarbDate, end: minCarbDate) + let carbRatio = carbRatioScheduleApplyingOverrideHistory.between(start: minCarbDate, end: maxCarbDate) + let insulinSensitivity = insulinSensitivityScheduleApplyingOverrideHistory.quantitiesBetween(start: minCarbDate, end: maxCarbDate) let effects = samples.map( to: effectVelocities, carbRatio: carbRatio, From 3f6d57d6687dc1fc3979491df107ad454cb4fbd2 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Wed, 6 Sep 2023 15:20:44 -0500 Subject: [PATCH 23/49] Fixing tests --- LoopTests/Managers/MealDetectionManagerTests.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/LoopTests/Managers/MealDetectionManagerTests.swift b/LoopTests/Managers/MealDetectionManagerTests.swift index bef5981395..3db48cc7eb 100644 --- a/LoopTests/Managers/MealDetectionManagerTests.swift +++ b/LoopTests/Managers/MealDetectionManagerTests.swift @@ -13,11 +13,14 @@ import LoopKit @testable import Loop fileprivate class MockGlucoseSample: GlucoseSampleValue { + let provenanceIdentifier = "" let isDisplayOnly: Bool let wasUserEntered: Bool let condition: LoopKit.GlucoseCondition? = nil let trendRate: HKQuantity? = nil + var trend: LoopKit.GlucoseTrend? + var syncIdentifier: String? let quantity: HKQuantity = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 100) let startDate: Date @@ -372,7 +375,7 @@ class MealDetectionManagerTests: XCTestCase { let updateGroup = DispatchGroup() updateGroup.enter() - mealDetectionManager.hasMissedMeal(insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in + mealDetectionManager.hasMissedMeal(glucoseSamples: glucoseSamples, insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in XCTAssertEqual(status, .hasMissedMeal(startTime: testType.missedMealDate!, carbAmount: 25)) updateGroup.leave() } From 2e16e1ea72ec75e1d2c3bc05069657838eb5bea4 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Wed, 6 Sep 2023 16:06:44 -0500 Subject: [PATCH 24/49] Revert test optimization; some pump tests that attempt ble comms are failing with the optimization --- Loop/AppDelegate.swift | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/Loop/AppDelegate.swift b/Loop/AppDelegate.swift index 41569632d3..5da6ce9cb6 100644 --- a/Loop/AppDelegate.swift +++ b/Loop/AppDelegate.swift @@ -22,11 +22,8 @@ final class AppDelegate: UIResponder, UIApplicationDelegate, WindowProvider { setenv("CFNETWORK_DIAGNOSTICS", "3", 1) - if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] == nil { - // Code only executes when not running tests - loopAppManager.initialize(windowProvider: self, launchOptions: launchOptions) - loopAppManager.launch() - } + loopAppManager.initialize(windowProvider: self, launchOptions: launchOptions) + loopAppManager.launch() return loopAppManager.isLaunchComplete } @@ -35,10 +32,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate, WindowProvider { func applicationDidBecomeActive(_ application: UIApplication) { log.default(#function) - if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] == nil { - // Code only executes when not running tests - loopAppManager.didBecomeActive() - } + loopAppManager.didBecomeActive() } func applicationWillResignActive(_ application: UIApplication) { From 29471dcd49027e341e29cb6fbbf0f6a7817ef99f Mon Sep 17 00:00:00 2001 From: Cameron Ingham Date: Thu, 7 Sep 2023 15:10:43 -0700 Subject: [PATCH 25/49] [LOOP-4721] UI Updates for Loop Alert Management --- .../hardware.imageset/Contents.json | 12 + .../hardware.imageset/Group 3403.pdf | Bin 0 -> 9522 bytes .../phone.imageset/Contents.json | 12 + .../phone.imageset/Group 3405.pdf | Bin 0 -> 1891 bytes Loop/Views/AlertManagementView.swift | 73 +++++- Loop/Views/HowMuteAlertWorkView.swift | 142 +++++++++--- Loop/Views/SettingsView.swift | 219 ++++++++++++------ 7 files changed, 349 insertions(+), 109 deletions(-) create mode 100644 Loop/DefaultAssets.xcassets/hardware.imageset/Contents.json create mode 100644 Loop/DefaultAssets.xcassets/hardware.imageset/Group 3403.pdf create mode 100644 Loop/DefaultAssets.xcassets/phone.imageset/Contents.json create mode 100644 Loop/DefaultAssets.xcassets/phone.imageset/Group 3405.pdf diff --git a/Loop/DefaultAssets.xcassets/hardware.imageset/Contents.json b/Loop/DefaultAssets.xcassets/hardware.imageset/Contents.json new file mode 100644 index 0000000000..579e60790c --- /dev/null +++ b/Loop/DefaultAssets.xcassets/hardware.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Group 3403.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Loop/DefaultAssets.xcassets/hardware.imageset/Group 3403.pdf b/Loop/DefaultAssets.xcassets/hardware.imageset/Group 3403.pdf new file mode 100644 index 0000000000000000000000000000000000000000..14057221edafce035c4c3188bdadf5d3fa536df1 GIT binary patch literal 9522 zcmeHNO^+ML5xvi^=!*b5fM@&r126={l57M?5M>o02P20KMM)cwyYVg^$ocho)tq_N zOA?lR%Rv;N9X@tfy{fLNuIZU)uU~%ujho9fIb+TJKmRou^X<3h>ea`?4{r`Phwbs3 z?|(Ko#?Hyg$2@!7P1oBiyiftxkujp=C?JUE5^-`ps0V5;G zyx`B>^kMq@@1{F(_i8T<|AtrmO=T#r&3>|T=pTeqbgtxtix!*p);wG^E%jb8nh852(dKqBB=Ahi}Obm+UQ`?-LG_=Ax0CN`DOrh z_GNS!`eEW1lHDsdvQk->QO@7#`~mxLf&YFAbBMvktTvI)@>9{DC~Y$-8G^e3bh8W5 z+G3WAI{0UqFuDvGd|cfpz8x2xTE;bB7$y_roKvnTC|6wr8nhXfRr&WIs5!M3Mv{yn zT5ut(Advw?hLU`P_AxhI^Q7T+SO5^$uXq^``Fn_ty#8O6zjShL5ZB(m+ zQKOHGnvH_$M$GRz{oq99xzUhRaEQ~@c9c*CBwa)@%8>zM2QuRLcPM&)#lO!je9ZIY z*X{Dtg^vrt0y;(|!Lht>$_`a^+5dPkV8i6qn4c!us^33;W+|sdkz3_&v546MlUAa- zJ-{S_jQriirE(X+!B7?DE}|ny{ue>oi7?_dFGg_SL`j!0<))GJnQ;`TIMEls!Rw6h z{X}BKb|})n5;4O#jTlPtw0IDWHl#OJ&D6ESaunkjf{LL~w3UOR*!|FsF1;G(CyfxQ zQB3(Dg*g&E&SD{CD(YA1s~%K(>QP3Dp&MG*yJ4inXScTuAUzbRMIq8b8@Mu4=AYSJ zp;uoU-SuK%D&1Ag_!Kl%A;)4fE=tR`)GytYIL>_OuB>6Wf9bAzG9f6wbXP}DXe0H1 z)?I1ggHRcH`8^Fu@sjFh?>Wn?sk-sj7rlk zMAa&W$>}H?bGRw!4yXc|r=wzmN|55z4DK+5Hi&T?3M_WA4T2X4X3`3)QIzA@FA(a- z1l>u=5Y&E0rLp~^);^U~`Xay?e$`LXs+AC?k{v;{p{&eDXwlSNuzG;@$UVryfEXEv zl@bwpYeLVgnlQe&pPJH$I>Jn0P zX9Q{faFc|a8_6E@PwNGBQXi2G8qURPV!WWF0io5z$cmC^4fPQbt2I1U3-^lAb&MFg zmtjgoTDFBfF@`)P5gAwv*y1fpqED%h$YvrdVuLk{qyeF~kmO`Y&mqKdaC=-Vf@@cc zyWQ0`h3HVE8?E6wt27N6Y9+Lp+A5eBD;Z@C2#p{{W|XA#sgRJYxpvg_DvrK5mdYR1i%O`L8Q{SvZ|F3D&&&0SxX(DML$DeglLC?3<8y=Hk{ikQSeUA zspY`t>XrZ0DTj6zdcz&C*eUxG&8J%nxzozo;{;1m%vEdTHq4b;vjbKV8WS8~0SYBH zoX6t^6eoin3Zb>qWG06DVsHkEM2$Z7si8TG3xQI^7mTw7H+4~H#b)TZsl-vm`cN)t)HFG z7At_n0jFAuoG*tEOY#^MTy{Z53o$y2yk1iKwxXm z5(hBD!}IWQjdFM_^G-Pkvotr~8f<{H5EGmyM==f$QW75I!9(7*iqaNhZl$}bBG9DR z`P^a>C*c1YM1#z84IaY5i)#oc1VzKcvM4rI_#Ba!<{Y~NFs8*&66T$A5N36TD+0nB z!HO`>5zKH@Qd`E;HxJraD2ImIH6N3eRe=JmZ^b%Q%me}#x+Xr8+8ce62MG?z=;Umv z#Do|FVuSUu0)Za_79V3$b}(P&opKOn*tMF4@#1I1mPK@7W0WY$*ct))90EiGg3jt> z9a9bf1#*ZdJgf?HMN&4gUp7W(ovUboY$_$egE(!0<0DRrBm`_k2q3(X5{MIwLP%O6 zc!5f1h~O$%_yB)HukWdVcGqKh5FQB(OLbU0__k(Hgv?fLkgSj>^vDB11%Hzi6PXHL zMaF>yOptLf*IMb?f^QK|X#iE^v2QX8F2!>9si#yr!-ROt6$ebH@^Zc;DS^8jXJgG3 zpmQO^nShYeu9FTGfrh6xfGkq62UGGD0S7gh8jzN9D1p!r5O+c~2WXL<8LkZQA?ig# z9`*YS2S-bA$hZ?KogtnXRM;$Iv8+frVOSAnaAF5-Ml>4PimgLb9&;kVVFEd%ZBKv} z3sC4HQY6Zz@1zi% z=Ri-4(`@PQ1JvZCc*qpQ05SkID$2eM2rGs*LQ27D)d^Y-oS z&3E@7&5y`~r3KAZ2fIqT-ag*{{IJt4@%-`jEvOo?;M%NuUflnTx$cbrBalH79vlpk#K1=?#A`o`;V*6uH@o}y zPp5u-*uI;rDbvw>`QPLu4HDqxBgITU=5B?g;d~0AV}UE4jH(UG{uFAc6Q>Y%GeU=l zo7>&?VUo9+{_qm+`0DX~zx~>Lb^G?~qf@S~@9z&r4DJrTdUN~VE!g*Wk8U0g-JFJo Mb9(md)nC5fjN5Qgvm6?3W79_rX0f5cK%iEb%EfLONNDh?r=wu`n2Y*JMC^^A9uacB?V zL*;#9&%86X-ySV5uTHfLLTFIXef=f?&d%WcTr}-E{S;=-i}y`?H{1gfT%}e0uxqLZ zQCv1ZH+8dma{-I%`LDVcKZKTQKR`{BQ=Yy2#-Hj3p(J`6l&76GSf>^6`o3=I^B6ct zZC%tJ(w31KY0(E0fhOszg)yr&kk%U){0!B?I9WPd;+$>;D(P&{M$gtFzDSZ0YpF$s zPD>lDEw3O$Y|g1fW|AFQ7R=$ zN6e@hMJ?&HFc8aGGGjT^ai%Ps8qN?UMi)yBISUn_EGnDYJ<4H}x;+YKM&YLVn9=0W zEsoEGWz^KwThWS7|N9`EbU~FifVI*aU!sP8zX&6PQ_ANn9MZu>Nr&Yr!1MwFot#m! z)MfByMvbN?;6gCzouMyFdOeI%Y-iB{9UVg_eHolKDImijt#XlWK+aK0YfX#@SqvkJ zH_2v}ZWbwsh`@~RFUM!XGG-T=^=|Ve=dw6@ZV8LXC?8t#!_n*}XG|t|a7=BSV#BnI z$w-S=ZQBh!+ZXTe z-K#O&qHBkyrHsk9C%CM3#K4Vc=I#lx>lg1fYmy47f)z}87TqK5)i3n-L}ro5qi2y) zfjuY8g)ge1+IDw;Z++S|w?aV>2j@ some View { + HStack(spacing: 16) { + Image(systemName: "circle.fill") + .resizable() + .frame(width: 8, height: 8) + .foregroundColor(color) + + self + } + } +} + struct HowMuteAlertWorkView_Previews: PreviewProvider { static var previews: some View { HowMuteAlertWorkView() diff --git a/Loop/Views/SettingsView.swift b/Loop/Views/SettingsView.swift index 7eadb7f1ec..825ec17820 100644 --- a/Loop/Views/SettingsView.swift +++ b/Loop/Views/SettingsView.swift @@ -24,13 +24,39 @@ public struct SettingsView: View { @ObservedObject var viewModel: SettingsViewModel @ObservedObject var versionUpdateViewModel: VersionUpdateViewModel - @State private var pumpChooserIsPresented: Bool = false - @State private var cgmChooserIsPresented: Bool = false - @State private var favoriteFoodsIsPresented: Bool = false - @State private var serviceChooserIsPresented: Bool = false - @State private var therapySettingsIsPresented: Bool = false - @State private var deletePumpDataAlertIsPresented = false - @State private var deleteCGMDataAlertIsPresented = false + enum Destination { + enum Alert: String, Identifiable { + var id: String { + rawValue + } + + case deleteCGMData + case deletePumpData + } + + enum ActionSheet: String, Identifiable { + var id: String { + rawValue + } + + case cgmPicker + case pumpPicker + case servicePicker + } + + enum Sheet: String, Identifiable { + var id: String { + rawValue + } + + case favoriteFoods + case therapySettings + } + } + + @State private var actionSheet: Destination.ActionSheet? + @State private var alert: Destination.Alert? + @State private var sheet: Destination.Sheet? var localizedAppNameAndVersion: String @@ -82,6 +108,57 @@ public struct SettingsView: View { .insetGroupedListStyle() .navigationBarTitle(Text(NSLocalizedString("Settings", comment: "Settings screen title"))) .navigationBarItems(trailing: dismissButton) + .actionSheet(item: $actionSheet) { actionSheet in + switch actionSheet { + case .cgmPicker: + ActionSheet( + title: Text("Add CGM", comment: "The title of the CGM chooser in settings"), + buttons: cgmChoices + ) + case .pumpPicker: + ActionSheet( + title: Text("Add Pump", comment: "The title of the pump chooser in settings"), + buttons: pumpChoices + ) + case .servicePicker: + ActionSheet( + title: Text("Add Service", comment: "The title of the add service action sheet in settings"), + buttons: serviceChoices + ) + } + } + .alert(item: $alert) { alert in + switch alert { + case .deleteCGMData: + makeDeleteAlert(for: self.viewModel.cgmManagerSettingsViewModel) + case .deletePumpData: + makeDeleteAlert(for: self.viewModel.pumpManagerSettingsViewModel) + } + } + .sheet(item: $sheet) { sheet in + switch sheet { + case .therapySettings: + TherapySettingsView( + mode: .settings, + viewModel: TherapySettingsViewModel( + therapySettings: viewModel.therapySettings(), + sensitivityOverridesEnabled: FeatureFlags.sensitivityOverridesEnabled, + adultChildInsulinModelSelectionEnabled: FeatureFlags.adultChildInsulinModelSelectionEnabled, + delegate: viewModel.therapySettingsViewModelDelegate + ) + ) + .environmentObject(displayGlucosePreference) + .environment(\.dismissAction, self.dismiss) + .environment(\.appName, self.appName) + .environment(\.chartColorPalette, .primary) + .environment(\.carbTintColor, self.carbTintColor) + .environment(\.glucoseTintColor, self.glucoseTintColor) + .environment(\.guidanceColors, self.guidanceColors) + .environment(\.insulinTintColor, self.insulinTintColor) + case .favoriteFoods: + FavoriteFoodsView() + } + } } .navigationViewStyle(.stack) } @@ -177,53 +254,46 @@ extension SettingsView { } } } + + @ViewBuilder + private var alertWarning: some View { + if viewModel.alertPermissionsChecker.showWarning || viewModel.alertPermissionsChecker.notificationCenterSettings.scheduledDeliveryEnabled { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundColor(.critical) + } else if viewModel.alertMuter.configuration.shouldMute { + Image(systemName: "speaker.slash.fill") + .foregroundColor(.white) + .padding(5) + .background(guidanceColors.warning) + .clipShape(RoundedRectangle(cornerRadius: 5, style: .continuous)) + } + } private var alertManagementSection: some View { Section { - NavigationLink(destination: AlertManagementView(checker: viewModel.alertPermissionsChecker, alertMuter: viewModel.alertMuter)) - { - HStack { - Text(NSLocalizedString("Alert Management", comment: "Alert Permissions button text")) - if viewModel.alertPermissionsChecker.showWarning || - viewModel.alertPermissionsChecker.notificationCenterSettings.scheduledDeliveryEnabled { - Spacer() - Image(systemName: "exclamationmark.triangle.fill") - .foregroundColor(.critical) - } else if viewModel.alertMuter.configuration.shouldMute { - Spacer() - Image(systemName: "speaker.slash.fill") - .foregroundColor(.white) - .padding(5) - .background(guidanceColors.warning) - .clipShape(RoundedRectangle(cornerRadius: 5, style: .continuous)) - } - } + NavigationLink(destination: AlertManagementView(checker: viewModel.alertPermissionsChecker, alertMuter: viewModel.alertMuter)) { + LargeButton( + action: {}, + includeArrow: false, + imageView: Image(systemName: "bell.fill") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 30), + secondaryImageView: alertWarning, + label: NSLocalizedString("Alert Management", comment: "Alert Permissions button text"), + descriptiveText: NSLocalizedString("Alert Permissions and Mute Alerts", comment: "Alert Permissions descriptive text") + ) } } } private var configurationSection: some View { Section(header: SectionHeader(label: NSLocalizedString("Configuration", comment: "The title of the Configuration section in settings"))) { - LargeButton(action: { self.therapySettingsIsPresented = true }, + LargeButton(action: { sheet = .therapySettings }, includeArrow: true, imageView: Image("Therapy Icon"), label: NSLocalizedString("Therapy Settings", comment: "Title text for button to Therapy Settings"), descriptiveText: NSLocalizedString("Diabetes Treatment", comment: "Descriptive text for Therapy Settings")) - .sheet(isPresented: $therapySettingsIsPresented) { - TherapySettingsView(mode: .settings, - viewModel: TherapySettingsViewModel(therapySettings: self.viewModel.therapySettings(), - sensitivityOverridesEnabled: FeatureFlags.sensitivityOverridesEnabled, - adultChildInsulinModelSelectionEnabled: FeatureFlags.adultChildInsulinModelSelectionEnabled, - delegate: self.viewModel.therapySettingsViewModelDelegate)) - .environmentObject(displayGlucosePreference) - .environment(\.dismissAction, self.dismiss) - .environment(\.appName, self.appName) - .environment(\.chartColorPalette, .primary) - .environment(\.carbTintColor, self.carbTintColor) - .environment(\.glucoseTintColor, self.glucoseTintColor) - .environment(\.guidanceColors, self.guidanceColors) - .environment(\.insulinTintColor, self.insulinTintColor) - } ForEach(pluginMenuItems.filter {$0.section == .configuration}) { item in item.view @@ -259,16 +329,11 @@ extension SettingsView { label: viewModel.pumpManagerSettingsViewModel.name(), descriptiveText: NSLocalizedString("Insulin Pump", comment: "Descriptive text for Insulin Pump")) } else if viewModel.isOnboardingComplete { - LargeButton(action: { self.pumpChooserIsPresented = true }, + LargeButton(action: { actionSheet = .pumpPicker }, includeArrow: false, imageView: plusImage, label: NSLocalizedString("Add Pump", comment: "Title text for button to add pump device"), descriptiveText: NSLocalizedString("Tap here to set up a pump", comment: "Descriptive text for button to add pump device")) - .actionSheet(isPresented: $pumpChooserIsPresented) { - ActionSheet(title: Text("Add Pump", comment: "The title of the pump chooser in settings"), buttons: pumpChoices) - } - } else { - EmptyView() } } @@ -291,28 +356,22 @@ extension SettingsView { label: viewModel.cgmManagerSettingsViewModel.name(), descriptiveText: NSLocalizedString("Continuous Glucose Monitor", comment: "Descriptive text for Continuous Glucose Monitor")) } else { - LargeButton(action: { self.cgmChooserIsPresented = true }, + LargeButton(action: { actionSheet = .cgmPicker }, includeArrow: false, imageView: plusImage, label: NSLocalizedString("Add CGM", comment: "Title text for button to add CGM device"), descriptiveText: NSLocalizedString("Tap here to set up a CGM", comment: "Descriptive text for button to add CGM device")) - .actionSheet(isPresented: $cgmChooserIsPresented) { - ActionSheet(title: Text("Add CGM", comment: "The title of the CGM chooser in settings"), buttons: cgmChoices) - } } } private var favoriteFoodsSection: some View { Section { - LargeButton(action: { self.favoriteFoodsIsPresented = true }, + LargeButton(action: { sheet = .favoriteFoods }, includeArrow: true, imageView: Image("Favorite Foods Icon").renderingMode(.template).foregroundColor(carbTintColor), label: "Favorite Foods", descriptiveText: "Simplify Carb Entry") } - .sheet(isPresented: $favoriteFoodsIsPresented) { - FavoriteFoodsView() - } } private var cgmChoices: [ActionSheet.Button] { @@ -337,14 +396,11 @@ extension SettingsView { descriptiveText: "") } if viewModel.servicesViewModel.inactiveServices().count > 0 { - LargeButton(action: { self.serviceChooserIsPresented = true }, + LargeButton(action: { actionSheet = .servicePicker }, includeArrow: false, imageView: plusImage, label: NSLocalizedString("Add Service", comment: "The title of the add service button in settings"), descriptiveText: NSLocalizedString("Tap here to set up a Service", comment: "The descriptive text of the add service button in settings")) - .actionSheet(isPresented: $serviceChooserIsPresented) { - ActionSheet(title: Text("Add Service", comment: "The title of the add service action sheet in settings"), buttons: serviceChoices) - } } } } @@ -362,28 +418,22 @@ extension SettingsView { private var deleteDataSection: some View { Section { if viewModel.pumpManagerSettingsViewModel.isTestingDevice { - Button(action: { self.deletePumpDataAlertIsPresented.toggle() }) { + Button(action: { alert = .deletePumpData }) { HStack { Spacer() Text("Delete Testing Pump Data").accentColor(.destructive) Spacer() } } - .alert(isPresented: $deletePumpDataAlertIsPresented) { - makeDeleteAlert(for: self.viewModel.pumpManagerSettingsViewModel) - } } if viewModel.cgmManagerSettingsViewModel.isTestingDevice { - Button(action: { self.deleteCGMDataAlertIsPresented.toggle() }) { + Button(action: { alert = .deleteCGMData }) { HStack { Spacer() Text("Delete Testing CGM Data").accentColor(.destructive) Spacer() } } - .alert(isPresented: $deleteCGMDataAlertIsPresented) { - makeDeleteAlert(for: self.viewModel.cgmManagerSettingsViewModel) - } } } } @@ -473,33 +523,60 @@ extension SettingsView { } } -fileprivate struct LargeButton: View { +fileprivate struct LargeButton: View { let action: () -> Void - var includeArrow: Bool = true + var includeArrow: Bool let imageView: Content + let secondaryImageView: SecondaryContent let label: String let descriptiveText: String + + init( + action: @escaping () -> Void, + includeArrow: Bool = true, + imageView: Content, + secondaryImageView: SecondaryContent = EmptyView(), + label: String, + descriptiveText: String + ) { + self.action = action + self.includeArrow = includeArrow + self.imageView = imageView + self.secondaryImageView = secondaryImageView + self.label = label + self.descriptiveText = descriptiveText + } // TODO: The design doesn't show this, but do we need to consider different values here for different size classes? private let spacing: CGFloat = 15 private let imageWidth: CGFloat = 60 private let imageHeight: CGFloat = 60 + private let secondaryImageWidth: CGFloat = 30 + private let secondaryImageHeight: CGFloat = 30 private let topBottomPadding: CGFloat = 10 public var body: some View { Button(action: action) { HStack { HStack(spacing: spacing) { - imageView.frame(width: imageWidth, height: imageHeight) + imageView.frame(maxWidth: imageWidth, maxHeight: imageHeight) VStack(alignment: .leading) { Text(label) .foregroundColor(.primary) DescriptiveText(label: descriptiveText) } } - if includeArrow { + + if !(secondaryImageView is EmptyView) || includeArrow { Spacer() + } + + if !(secondaryImageView is EmptyView) { + secondaryImageView.frame(width: secondaryImageWidth, height: secondaryImageHeight) + } + + if includeArrow { // TODO: Ick. I can't use a NavigationLink because we're not Navigating, but this seems worse somehow. Image(systemName: "chevron.right").foregroundColor(.gray).font(.footnote) } From ea66ab2f34f0bb34a4a34ee71633f87873bc8932 Mon Sep 17 00:00:00 2001 From: Billy Booth Date: Sat, 9 Sep 2023 12:47:00 -0500 Subject: [PATCH 26/49] Add Hindi Intents --- Common/hi.lproj/Intents.strings | 24 ++++++++++++++++++++++++ Loop.xcodeproj/project.pbxproj | 2 ++ 2 files changed, 26 insertions(+) create mode 100644 Common/hi.lproj/Intents.strings diff --git a/Common/hi.lproj/Intents.strings b/Common/hi.lproj/Intents.strings new file mode 100644 index 0000000000..853af215c0 --- /dev/null +++ b/Common/hi.lproj/Intents.strings @@ -0,0 +1,24 @@ +"80eo5o" = "Add Carb Entry"; + +"9KhaIS" = "I've set the preset"; + +"I4OZy8" = "Enable Override Preset"; + +"OcNxIj" = "Add Carb Entry"; + +"XNNmtH" = "Enable preset in Loop"; + +"ZZ3mtM" = "Enable an override preset in Loop"; + +"b085BW" = "I wasn't able to set the preset."; + +"lYMuWV" = "Override Name"; + +"nDKAmn" = "What's the name of the override you'd like to set?"; + +"oLQSsJ" = "Enable '${overrideName}' Override Preset"; + +"yBzwCL" = "Override Selection"; + +"yc02Yq" = "Add a carb entry to Loop"; + diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 0bc7b03ff7..83abfd98b4 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -787,6 +787,7 @@ 1DC63E7325351BDF004605DA /* TrueTime.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = TrueTime.framework; path = Carthage/Build/iOS/TrueTime.framework; sourceTree = ""; }; 1DE09BA824A3E23F009EE9F9 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 1DFE9E162447B6270082C280 /* UserNotificationAlertSchedulerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationAlertSchedulerTests.swift; sourceTree = ""; }; + 3D03C6DA2AACE6AC00FDE5D2 /* hi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hi; path = hi.lproj/Intents.strings; sourceTree = ""; }; 4302F4E01D4E9C8900F0FCAF /* TextFieldTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldTableViewController.swift; sourceTree = ""; }; 4302F4E21D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InsulinDeliveryTableViewController.swift; sourceTree = ""; }; 430B29892041F54A00BA9F93 /* NSUserDefaults.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSUserDefaults.swift; sourceTree = ""; }; @@ -4224,6 +4225,7 @@ C1C3127F297E4C0400296DA4 /* ar */, C1C247882995823200371B88 /* sk */, C1C5357529C6346A00E32DF9 /* cs */, + 3D03C6DA2AACE6AC00FDE5D2 /* hi */, ); name = Intents.intentdefinition; sourceTree = ""; From 232b21d071d7663c9f7946d2e101b372fefe7e12 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Sat, 9 Sep 2023 19:14:20 -0500 Subject: [PATCH 27/49] Update build destinations --- Loop.xcodeproj/project.pbxproj | 36 +++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 83abfd98b4..dacc5be194 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -24,7 +24,7 @@ 149A28E42A8A63A700052EDF /* FavoriteFoodDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149A28E32A8A63A700052EDF /* FavoriteFoodDetailView.swift */; }; 14B1735E28AED9EC006CCD7C /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14B1735D28AED9EC006CCD7C /* WidgetKit.framework */; }; 14B1736028AED9EC006CCD7C /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14B1735F28AED9EC006CCD7C /* SwiftUI.framework */; }; - 14B1736928AED9EE006CCD7C /* SmallStatusWidgetExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 14B1735C28AED9EC006CCD7C /* SmallStatusWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 14B1736928AED9EE006CCD7C /* Loop Widget Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 14B1735C28AED9EC006CCD7C /* Loop Widget Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 14B1737228AEDBF6006CCD7C /* BasalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B1736E28AEDBF6006CCD7C /* BasalView.swift */; }; 14B1737328AEDBF6006CCD7C /* SystemStatusWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B1736F28AEDBF6006CCD7C /* SystemStatusWidget.swift */; }; 14B1737428AEDBF6006CCD7C /* GlucoseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B1737028AEDBF6006CCD7C /* GlucoseView.swift */; }; @@ -719,7 +719,7 @@ dstPath = ""; dstSubfolderSpec = 13; files = ( - 14B1736928AED9EE006CCD7C /* SmallStatusWidgetExtension.appex in Embed App Extensions */, + 14B1736928AED9EE006CCD7C /* Loop Widget Extension.appex in Embed App Extensions */, E9B07F94253BBA6500BAD8F8 /* Loop Intent Extension.appex in Embed App Extensions */, 4F70C1E81DE8DCA7006380B7 /* Loop Status Extension.appex in Embed App Extensions */, ); @@ -751,7 +751,7 @@ 149A28BA2A853E5100052EDF /* CarbEntryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbEntryViewModel.swift; sourceTree = ""; }; 149A28BC2A853E6C00052EDF /* CarbEntryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbEntryView.swift; sourceTree = ""; }; 149A28E32A8A63A700052EDF /* FavoriteFoodDetailView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FavoriteFoodDetailView.swift; sourceTree = ""; }; - 14B1735C28AED9EC006CCD7C /* SmallStatusWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; name = SmallStatusWidgetExtension.appex; path = "Loop Widget Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; + 14B1735C28AED9EC006CCD7C /* Loop Widget Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Loop Widget Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 14B1735D28AED9EC006CCD7C /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; 14B1735F28AED9EC006CCD7C /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; 14B1736628AED9EE006CCD7C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -1993,7 +1993,7 @@ 43D9FFCF21EAE05D00AF44BF /* LoopCore.framework */, 43D9002A21EB209400AF44BF /* LoopCore.framework */, E9B07F7C253BBA6500BAD8F8 /* Loop Intent Extension.appex */, - 14B1735C28AED9EC006CCD7C /* SmallStatusWidgetExtension.appex */, + 14B1735C28AED9EC006CCD7C /* Loop Widget Extension.appex */, ); name = Products; sourceTree = ""; @@ -2981,7 +2981,7 @@ ); name = "Loop Widget Extension"; productName = SmallStatusWidgetExtension; - productReference = 14B1735C28AED9EC006CCD7C /* SmallStatusWidgetExtension.appex */; + productReference = 14B1735C28AED9EC006CCD7C /* Loop Widget Extension.appex */; productType = "com.apple.product-type.app-extension"; }; 43776F8B1B8022E90074EA36 /* Loop */ = { @@ -4827,6 +4827,10 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "$(LOOP_PROVISIONING_PROFILE_SPECIFIER_WIDGET_EXTENSION_DEBUG)"; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -4871,6 +4875,10 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "$(LOOP_PROVISIONING_PROFILE_SPECIFIER_WIDGET_EXTENSION_RELEASE)"; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; @@ -5128,6 +5136,7 @@ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; TARGETED_DEVICE_FAMILY = 1; }; name = Debug; @@ -5154,6 +5163,7 @@ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; TARGETED_DEVICE_FAMILY = 1; }; name = Release; @@ -5407,6 +5417,10 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "$(LOOP_PROVISIONING_PROFILE_SPECIFIER_STATUS_EXTENSION_DEBUG)"; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; TARGETED_DEVICE_FAMILY = 1; }; name = Debug; @@ -5429,6 +5443,10 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "$(LOOP_PROVISIONING_PROFILE_SPECIFIER_STATUS_EXTENSION_RELEASE)"; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; TARGETED_DEVICE_FAMILY = 1; }; name = Release; @@ -5492,6 +5510,10 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "$(LOOP_PROVISIONING_PROFILE_SPECIFIER_INTENT_EXTENSION_DEBUG)"; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; TARGETED_DEVICE_FAMILY = 1; }; name = Debug; @@ -5515,6 +5537,10 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "$(LOOP_PROVISIONING_PROFILE_SPECIFIER_INTENT_EXTENSION_RELEASE)"; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; TARGETED_DEVICE_FAMILY = 1; }; name = Release; From d76c009fef3b2439f52b8f74defcad42d7656d22 Mon Sep 17 00:00:00 2001 From: Cameron Ingham Date: Wed, 13 Sep 2023 10:56:13 -0700 Subject: [PATCH 28/49] [LOOP-4721] UI Updates for Loop Alert Management --- Loop/Views/SettingsView.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Loop/Views/SettingsView.swift b/Loop/Views/SettingsView.swift index 825ec17820..47cc61041b 100644 --- a/Loop/Views/SettingsView.swift +++ b/Loop/Views/SettingsView.swift @@ -111,17 +111,17 @@ public struct SettingsView: View { .actionSheet(item: $actionSheet) { actionSheet in switch actionSheet { case .cgmPicker: - ActionSheet( + return ActionSheet( title: Text("Add CGM", comment: "The title of the CGM chooser in settings"), buttons: cgmChoices ) case .pumpPicker: - ActionSheet( + return ActionSheet( title: Text("Add Pump", comment: "The title of the pump chooser in settings"), buttons: pumpChoices ) case .servicePicker: - ActionSheet( + return ActionSheet( title: Text("Add Service", comment: "The title of the add service action sheet in settings"), buttons: serviceChoices ) @@ -130,15 +130,15 @@ public struct SettingsView: View { .alert(item: $alert) { alert in switch alert { case .deleteCGMData: - makeDeleteAlert(for: self.viewModel.cgmManagerSettingsViewModel) + return makeDeleteAlert(for: self.viewModel.cgmManagerSettingsViewModel) case .deletePumpData: - makeDeleteAlert(for: self.viewModel.pumpManagerSettingsViewModel) + return makeDeleteAlert(for: self.viewModel.pumpManagerSettingsViewModel) } } .sheet(item: $sheet) { sheet in switch sheet { case .therapySettings: - TherapySettingsView( + return TherapySettingsView( mode: .settings, viewModel: TherapySettingsViewModel( therapySettings: viewModel.therapySettings(), @@ -156,7 +156,7 @@ public struct SettingsView: View { .environment(\.guidanceColors, self.guidanceColors) .environment(\.insulinTintColor, self.insulinTintColor) case .favoriteFoods: - FavoriteFoodsView() + return FavoriteFoodsView() } } } From 2741c8e50a5f5293fecdd08959ce077d02c039ca Mon Sep 17 00:00:00 2001 From: Cameron Ingham Date: Wed, 13 Sep 2023 10:58:08 -0700 Subject: [PATCH 29/49] [LOOP-4721] UI Updates for Loop Alert Management --- Loop/Views/SettingsView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Loop/Views/SettingsView.swift b/Loop/Views/SettingsView.swift index 47cc61041b..b8e9c1daf7 100644 --- a/Loop/Views/SettingsView.swift +++ b/Loop/Views/SettingsView.swift @@ -138,7 +138,7 @@ public struct SettingsView: View { .sheet(item: $sheet) { sheet in switch sheet { case .therapySettings: - return TherapySettingsView( + TherapySettingsView( mode: .settings, viewModel: TherapySettingsViewModel( therapySettings: viewModel.therapySettings(), @@ -156,7 +156,7 @@ public struct SettingsView: View { .environment(\.guidanceColors, self.guidanceColors) .environment(\.insulinTintColor, self.insulinTintColor) case .favoriteFoods: - return FavoriteFoodsView() + FavoriteFoodsView() } } } From 974574de7237e5691d5c995985bbd92ec705167a Mon Sep 17 00:00:00 2001 From: Nathaniel Hamming Date: Tue, 19 Sep 2023 05:45:56 -0300 Subject: [PATCH 30/49] [COASTAL-1291] added tidepool security plugin (#593) * added tidepool security plugin * refactor to provide security provider to pump manager * response to comments * response to PR comments * response to PR comment * fixed unit tests * all active plugins * corrected typo * corrected file name --- Common/Models/PumpManager.swift | 6 +- Loop.xcodeproj/project.pbxproj | 8 ++ Loop/Managers/AnalyticsServicesManager.swift | 2 +- Loop/Managers/CGMManager.swift | 6 +- Loop/Managers/DeviceDataManager.swift | 98 +++++++++++--- Loop/Managers/LoggingServicesManager.swift | 2 +- Loop/Managers/LoopAppManager.swift | 8 +- Loop/Managers/OnboardingManager.swift | 47 +++++-- Loop/Managers/RemoteDataServicesManager.swift | 24 ++-- Loop/Managers/Service.swift | 17 +-- Loop/Managers/ServicesManager.swift | 31 +++-- Loop/Managers/StatefulPluggable.swift | 20 +++ Loop/Managers/StatefulPluginManager.swift | 125 ++++++++++++++++++ Loop/Managers/SupportManager.swift | 14 +- Loop/Managers/TestingScenariosManager.swift | 8 +- Loop/Plugins/PluginManager.swift | 35 ++++- .../StatusTableViewController.swift | 2 +- Loop/View Models/ServicesViewModel.swift | 14 +- Loop/Views/SettingsView.swift | 2 +- LoopTests/Managers/DoseEnactorTests.swift | 2 +- LoopTests/Managers/SupportManagerTests.swift | 6 +- 21 files changed, 365 insertions(+), 112 deletions(-) create mode 100644 Loop/Managers/StatefulPluggable.swift create mode 100644 Loop/Managers/StatefulPluginManager.swift diff --git a/Common/Models/PumpManager.swift b/Common/Models/PumpManager.swift index 3c2486ef68..5ec574366c 100644 --- a/Common/Models/PumpManager.swift +++ b/Common/Models/PumpManager.swift @@ -12,13 +12,13 @@ import MockKit import MockKitUI let staticPumpManagersByIdentifier: [String: PumpManagerUI.Type] = [ - MockPumpManager.managerIdentifier : MockPumpManager.self + MockPumpManager.pluginIdentifier : MockPumpManager.self ] var availableStaticPumpManagers: [PumpManagerDescriptor] { if FeatureFlags.allowSimulators { return [ - PumpManagerDescriptor(identifier: MockPumpManager.managerIdentifier, localizedTitle: MockPumpManager.localizedTitle) + PumpManagerDescriptor(identifier: MockPumpManager.pluginIdentifier, localizedTitle: MockPumpManager.localizedTitle) ] } else { return [] @@ -31,7 +31,7 @@ extension PumpManager { var rawValue: RawValue { return [ - "managerIdentifier": self.managerIdentifier, + "managerIdentifier": self.pluginIdentifier, "state": self.rawState ] } diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index a5426f692c..eab2ef9b28 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -374,6 +374,7 @@ B42D124328D371C400E43D22 /* AlertMuter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B42D124228D371C400E43D22 /* AlertMuter.swift */; }; B43CF07E29434EC4008A520B /* HowMuteAlertWorkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B43CF07D29434EC4008A520B /* HowMuteAlertWorkView.swift */; }; B43DA44124D9C12100CAFF4E /* DismissibleHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B43DA44024D9C12100CAFF4E /* DismissibleHostingController.swift */; }; + B470F5842AB22B5100049695 /* StatefulPluggable.swift in Sources */ = {isa = PBXBuildFile; fileRef = B470F5832AB22B5100049695 /* StatefulPluggable.swift */; }; B48B0BAC24900093009A48DE /* PumpStatusHUDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B48B0BAB24900093009A48DE /* PumpStatusHUDView.swift */; }; B490A03F24D0550F00F509FA /* GlucoseRangeCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B490A03E24D0550F00F509FA /* GlucoseRangeCategory.swift */; }; B490A04124D0559D00F509FA /* DeviceLifecycleProgressState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B490A04024D0559D00F509FA /* DeviceLifecycleProgressState.swift */; }; @@ -387,6 +388,7 @@ B4CAD8792549D2540057946B /* LoopCompletionFreshnessTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4CAD8782549D2540057946B /* LoopCompletionFreshnessTests.swift */; }; B4D4534128E5CA7900F1A8D9 /* AlertMuterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D4534028E5CA7900F1A8D9 /* AlertMuterTests.swift */; }; B4D620D424D9EDB900043B3C /* GuidanceColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D620D324D9EDB900043B3C /* GuidanceColors.swift */; }; + B4D904412AA8989100CBD826 /* StatefulPluginManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D904402AA8989100CBD826 /* StatefulPluginManager.swift */; }; B4E202302661063E009421B5 /* AutomaticDosingStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4E2022F2661063E009421B5 /* AutomaticDosingStatus.swift */; }; B4E96D4B248A6B6E002DABAD /* DeviceStatusHUDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4E96D4A248A6B6E002DABAD /* DeviceStatusHUDView.swift */; }; B4E96D4F248A6E20002DABAD /* CGMStatusHUDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4E96D4E248A6E20002DABAD /* CGMStatusHUDView.swift */; }; @@ -1297,6 +1299,7 @@ B42D124228D371C400E43D22 /* AlertMuter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertMuter.swift; sourceTree = ""; }; B43CF07D29434EC4008A520B /* HowMuteAlertWorkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HowMuteAlertWorkView.swift; sourceTree = ""; }; B43DA44024D9C12100CAFF4E /* DismissibleHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissibleHostingController.swift; sourceTree = ""; }; + B470F5832AB22B5100049695 /* StatefulPluggable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatefulPluggable.swift; sourceTree = ""; }; B48B0BAB24900093009A48DE /* PumpStatusHUDView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpStatusHUDView.swift; sourceTree = ""; }; B490A03C24D04F9400F509FA /* Color.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = ""; }; B490A03E24D0550F00F509FA /* GlucoseRangeCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseRangeCategory.swift; sourceTree = ""; }; @@ -1307,6 +1310,7 @@ B4CAD8782549D2540057946B /* LoopCompletionFreshnessTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopCompletionFreshnessTests.swift; sourceTree = ""; }; B4D4534028E5CA7900F1A8D9 /* AlertMuterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertMuterTests.swift; sourceTree = ""; }; B4D620D324D9EDB900043B3C /* GuidanceColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuidanceColors.swift; sourceTree = ""; }; + B4D904402AA8989100CBD826 /* StatefulPluginManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatefulPluginManager.swift; sourceTree = ""; }; B4E2022F2661063E009421B5 /* AutomaticDosingStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutomaticDosingStatus.swift; sourceTree = ""; }; B4E96D4A248A6B6E002DABAD /* DeviceStatusHUDView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStatusHUDView.swift; sourceTree = ""; }; B4E96D4E248A6E20002DABAD /* CGMStatusHUDView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGMStatusHUDView.swift; sourceTree = ""; }; @@ -2293,10 +2297,12 @@ 43C094491CACCC73001F6403 /* NotificationManager.swift */, A97F250725E056D500F0EE19 /* OnboardingManager.swift */, 432E73CA1D24B3D6009AD15D /* RemoteDataServicesManager.swift */, + B4D904402AA8989100CBD826 /* StatefulPluginManager.swift */, A9C62D852331703000535612 /* Service.swift */, A9C62D872331703000535612 /* ServicesManager.swift */, C1F7822527CC056900C0919A /* SettingsManager.swift */, E9BB27AA23B85C3500FB4987 /* SleepStore.swift */, + B470F5832AB22B5100049695 /* StatefulPluggable.swift */, 43FCEEA8221A615B0013DD30 /* StatusChartsManager.swift */, 1D63DEA426E950D400F46FA5 /* SupportManager.swift */, 4F70C20F1DE8FAC5006380B7 /* ExtensionDataManager.swift */, @@ -3653,10 +3659,12 @@ B4FEEF7D24B8A71F00A8DF9B /* DeviceDataManager+DeviceStatus.swift in Sources */, 142CB7592A60BF2E0075748A /* EditMode.swift in Sources */, E95D380324EADF36005E2F50 /* CarbStoreProtocol.swift in Sources */, + B470F5842AB22B5100049695 /* StatefulPluggable.swift in Sources */, E98A55ED24EDD6380008715D /* LatestStoredSettingsProvider.swift in Sources */, C1FB428F217921D600FAB378 /* PumpManagerUI.swift in Sources */, A9B996F227238705002DC09C /* DosingDecisionStore.swift in Sources */, 43C513191E864C4E001547C7 /* GlucoseRangeSchedule.swift in Sources */, + B4D904412AA8989100CBD826 /* StatefulPluginManager.swift in Sources */, E9B355292935919E0076AB04 /* MissedMealSettings.swift in Sources */, 43A51E1F1EB6D62A000736CC /* CarbAbsorptionViewController.swift in Sources */, 43776F901B8022E90074EA36 /* AppDelegate.swift in Sources */, diff --git a/Loop/Managers/AnalyticsServicesManager.swift b/Loop/Managers/AnalyticsServicesManager.swift index 650d74a597..808a34c81a 100644 --- a/Loop/Managers/AnalyticsServicesManager.swift +++ b/Loop/Managers/AnalyticsServicesManager.swift @@ -28,7 +28,7 @@ final class AnalyticsServicesManager { } func removeService(_ analyticsService: AnalyticsService) { - analyticsServices.removeAll { $0.serviceIdentifier == analyticsService.serviceIdentifier } + analyticsServices.removeAll { $0.pluginIdentifier == analyticsService.pluginIdentifier } } private func logEvent(_ name: String, withProperties properties: [AnyHashable: Any]? = nil, outOfSession: Bool = false) { diff --git a/Loop/Managers/CGMManager.swift b/Loop/Managers/CGMManager.swift index 041e632288..fe39e3926c 100644 --- a/Loop/Managers/CGMManager.swift +++ b/Loop/Managers/CGMManager.swift @@ -10,13 +10,13 @@ import LoopKitUI import MockKit let staticCGMManagersByIdentifier: [String: CGMManager.Type] = [ - MockCGMManager.managerIdentifier: MockCGMManager.self + MockCGMManager.pluginIdentifier: MockCGMManager.self ] var availableStaticCGMManagers: [CGMManagerDescriptor] { if FeatureFlags.allowSimulators { return [ - CGMManagerDescriptor(identifier: MockCGMManager.managerIdentifier, localizedTitle: MockCGMManager.localizedTitle) + CGMManagerDescriptor(identifier: MockCGMManager.pluginIdentifier, localizedTitle: MockCGMManager.localizedTitle) ] } else { return [] @@ -40,7 +40,7 @@ extension CGMManager { var rawValue: [String: Any] { return [ - "managerIdentifier": managerIdentifier, + "managerIdentifier": pluginIdentifier, "state": self.rawState ] } diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 2e8d157531..2697df7569 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -97,9 +97,9 @@ final class DeviceDataManager { dispatchPrecondition(condition: .onQueue(.main)) setupCGM() - if cgmManager?.managerIdentifier != oldValue?.managerIdentifier { + if cgmManager?.pluginIdentifier != oldValue?.pluginIdentifier { if let cgmManager = cgmManager { - analyticsServicesManager.cgmWasAdded(identifier: cgmManager.managerIdentifier) + analyticsServicesManager.cgmWasAdded(identifier: cgmManager.pluginIdentifier) } else { analyticsServicesManager.cgmWasRemoved() } @@ -125,9 +125,9 @@ final class DeviceDataManager { cgmManager = nil } - if pumpManager?.managerIdentifier != oldValue?.managerIdentifier { + if pumpManager?.pluginIdentifier != oldValue?.pluginIdentifier { if let pumpManager = pumpManager { - analyticsServicesManager.pumpWasAdded(identifier: pumpManager.managerIdentifier) + analyticsServicesManager.pumpWasAdded(identifier: pumpManager.pluginIdentifier) } else { analyticsServicesManager.pumpWasRemoved() } @@ -205,6 +205,8 @@ final class DeviceDataManager { sleepDataAuthorizationRequired } + private(set) var statefulPluginManager: StatefulPluginManager! + // MARK: Services private(set) var servicesManager: ServicesManager! @@ -422,7 +424,9 @@ final class DeviceDataManager { servicesManagerDelegate: loopManager, servicesManagerDosingDelegate: self ) - + + statefulPluginManager = StatefulPluginManager(pluginManager: pluginManager, servicesManager: servicesManager) + let criticalEventLogs: [CriticalEventLog] = [settingsManager.settingsStore, glucoseStore, carbStore, dosingDecisionStore, doseStore, deviceLog, alertManager.alertStore] criticalEventLogExportManager = CriticalEventLogExportManager(logs: criticalEventLogs, directory: FileManager.default.exportsDirectoryURL, @@ -582,7 +586,7 @@ final class DeviceDataManager { var availableCGMManagers: [CGMManagerDescriptor] { var availableCGMManagers = pluginManager.availableCGMManagers + availableStaticCGMManagers if let pumpManagerAsCGMManager = pumpManager as? CGMManager { - availableCGMManagers.append(CGMManagerDescriptor(identifier: pumpManagerAsCGMManager.managerIdentifier, localizedTitle: pumpManagerAsCGMManager.localizedTitle)) + availableCGMManagers.append(CGMManagerDescriptor(identifier: pumpManagerAsCGMManager.pluginIdentifier, localizedTitle: pumpManagerAsCGMManager.localizedTitle)) } availableCGMManagers = availableCGMManagers.filter({ cgmManager in @@ -635,7 +639,7 @@ final class DeviceDataManager { } public func setupCGMManagerFromPumpManager(withIdentifier identifier: String) -> CGMManager? { - guard identifier == pumpManager?.managerIdentifier, let cgmManager = pumpManager as? CGMManager else { + guard identifier == pumpManager?.pluginIdentifier, let cgmManager = pumpManager as? CGMManager else { return nil } @@ -698,19 +702,20 @@ private extension DeviceDataManager { cgmManager?.cgmManagerDelegate = self cgmManager?.delegateQueue = queue + reportPluginInitializationComplete() glucoseStore.managedDataInterval = cgmManager?.managedDataInterval glucoseStore.healthKitStorageDelay = cgmManager.map{ type(of: $0).healthKitStorageDelay } ?? 0 updatePumpManagerBLEHeartbeatPreference() if let cgmManager = cgmManager { - alertManager?.addAlertResponder(managerIdentifier: cgmManager.managerIdentifier, + alertManager?.addAlertResponder(managerIdentifier: cgmManager.pluginIdentifier, alertResponder: cgmManager) - alertManager?.addAlertSoundVendor(managerIdentifier: cgmManager.managerIdentifier, + alertManager?.addAlertSoundVendor(managerIdentifier: cgmManager.pluginIdentifier, soundVendor: cgmManager) cgmHasValidSensorSession = cgmManager.cgmManagerStatus.hasValidSensorSession - analyticsServicesManager.identifyCGMType(cgmManager.managerIdentifier) + analyticsServicesManager.identifyCGMType(cgmManager.pluginIdentifier) } if let cgmManagerUI = cgmManager as? CGMManagerUI { @@ -723,6 +728,7 @@ private extension DeviceDataManager { pumpManager?.pumpManagerDelegate = self pumpManager?.delegateQueue = queue + reportPluginInitializationComplete() doseStore.device = pumpManager?.status.device pumpManagerHUDProvider = pumpManager?.hudProvider(bluetoothProvider: bluetoothProvider, colorPalette: .default, allowedInsulinTypes: allowedInsulinTypes) @@ -732,14 +738,14 @@ private extension DeviceDataManager { doseStore.pumpRecordsBasalProfileStartEvents = pumpRecordsBasalProfileStartEvents } if let pumpManager = pumpManager { - alertManager?.addAlertResponder(managerIdentifier: pumpManager.managerIdentifier, + alertManager?.addAlertResponder(managerIdentifier: pumpManager.pluginIdentifier, alertResponder: pumpManager) - alertManager?.addAlertSoundVendor(managerIdentifier: pumpManager.managerIdentifier, + alertManager?.addAlertSoundVendor(managerIdentifier: pumpManager.pluginIdentifier, soundVendor: pumpManager) deliveryUncertaintyAlertManager = DeliveryUncertaintyAlertManager(pumpManager: pumpManager, alertPresenter: alertPresenter) - analyticsServicesManager.identifyPumpType(pumpManager.managerIdentifier) + analyticsServicesManager.identifyPumpType(pumpManager.pluginIdentifier) } } @@ -750,6 +756,58 @@ private extension DeviceDataManager { } } +// MARK: - Plugins +extension DeviceDataManager { + func reportPluginInitializationComplete() { + let allActivePlugins = self.allActivePlugins + + for plugin in servicesManager.activeServices { + plugin.initializationComplete(for: allActivePlugins) + } + + for plugin in statefulPluginManager.activeStatefulPlugins { + plugin.initializationComplete(for: allActivePlugins) + } + + for plugin in availableSupports { + plugin.initializationComplete(for: allActivePlugins) + } + + cgmManager?.initializationComplete(for: allActivePlugins) + pumpManager?.initializationComplete(for: allActivePlugins) + } + + var allActivePlugins: [Pluggable] { + var allActivePlugins: [Pluggable] = servicesManager.activeServices + + for plugin in statefulPluginManager.activeStatefulPlugins { + if !allActivePlugins.contains(where: { $0.pluginIdentifier == plugin.pluginIdentifier }) { + allActivePlugins.append(plugin) + } + } + + for plugin in availableSupports { + if !allActivePlugins.contains(where: { $0.pluginIdentifier == plugin.pluginIdentifier }) { + allActivePlugins.append(plugin) + } + } + + if let cgmManager = cgmManager { + if !allActivePlugins.contains(where: { $0.pluginIdentifier == cgmManager.pluginIdentifier }) { + allActivePlugins.append(cgmManager) + } + } + + if let pumpManager = pumpManager { + if !allActivePlugins.contains(where: { $0.pluginIdentifier == pumpManager.pluginIdentifier }) { + allActivePlugins.append(pumpManager) + } + } + + return allActivePlugins + } +} + // MARK: - Client API extension DeviceDataManager { func enactBolus(units: Double, activationType: BolusActivationType, completion: @escaping (_ error: Error?) -> Void = { _ in }) { @@ -861,7 +919,7 @@ extension DeviceDataManager { extension DeviceDataManager: DeviceManagerDelegate { func deviceManager(_ manager: DeviceManager, logEventForDeviceIdentifier deviceIdentifier: String?, type: DeviceLogEntryType, message: String, completion: ((Error?) -> Void)?) { - deviceLog.log(managerIdentifier: manager.managerIdentifier, deviceIdentifier: deviceIdentifier, type: type, message: message, completion: completion) + deviceLog.log(managerIdentifier: manager.pluginIdentifier, deviceIdentifier: deviceIdentifier, type: type, message: message, completion: completion) } var allowDebugFeatures: Bool { @@ -909,7 +967,7 @@ extension DeviceDataManager: CGMManagerDelegate { func cgmManagerWantsDeletion(_ manager: CGMManager) { dispatchPrecondition(condition: .onQueue(queue)) - log.default("CGM manager with identifier '%{public}@' wants deletion", manager.managerIdentifier) + log.default("CGM manager with identifier '%{public}@' wants deletion", manager.pluginIdentifier) DispatchQueue.main.async { if let cgmManagerUI = self.cgmManager as? CGMManagerUI { @@ -962,13 +1020,13 @@ extension DeviceDataManager: CGMManagerDelegate { extension DeviceDataManager: CGMManagerOnboardingDelegate { func cgmManagerOnboarding(didCreateCGMManager cgmManager: CGMManagerUI) { - log.default("CGM manager with identifier '%{public}@' created", cgmManager.managerIdentifier) + log.default("CGM manager with identifier '%{public}@' created", cgmManager.pluginIdentifier) self.cgmManager = cgmManager } func cgmManagerOnboarding(didOnboardCGMManager cgmManager: CGMManagerUI) { precondition(cgmManager.isOnboarded) - log.default("CGM manager with identifier '%{public}@' onboarded", cgmManager.managerIdentifier) + log.default("CGM manager with identifier '%{public}@' onboarded", cgmManager.pluginIdentifier) DispatchQueue.main.async { self.refreshDeviceData() @@ -1101,7 +1159,7 @@ extension DeviceDataManager: PumpManagerDelegate { func pumpManagerWillDeactivate(_ pumpManager: PumpManager) { dispatchPrecondition(condition: .onQueue(queue)) - log.default("Pump manager with identifier '%{public}@' will deactivate", pumpManager.managerIdentifier) + log.default("Pump manager with identifier '%{public}@' will deactivate", pumpManager.pluginIdentifier) DispatchQueue.main.async { self.pumpManager = nil @@ -1170,13 +1228,13 @@ extension DeviceDataManager: PumpManagerDelegate { extension DeviceDataManager: PumpManagerOnboardingDelegate { func pumpManagerOnboarding(didCreatePumpManager pumpManager: PumpManagerUI) { - log.default("Pump manager with identifier '%{public}@' created", pumpManager.managerIdentifier) + log.default("Pump manager with identifier '%{public}@' created", pumpManager.pluginIdentifier) self.pumpManager = pumpManager } func pumpManagerOnboarding(didOnboardPumpManager pumpManager: PumpManagerUI) { precondition(pumpManager.isOnboarded) - log.default("Pump manager with identifier '%{public}@' onboarded", pumpManager.managerIdentifier) + log.default("Pump manager with identifier '%{public}@' onboarded", pumpManager.pluginIdentifier) DispatchQueue.main.async { self.refreshDeviceData() diff --git a/Loop/Managers/LoggingServicesManager.swift b/Loop/Managers/LoggingServicesManager.swift index 25b63ac1f9..287371aa01 100644 --- a/Loop/Managers/LoggingServicesManager.swift +++ b/Loop/Managers/LoggingServicesManager.swift @@ -24,7 +24,7 @@ final class LoggingServicesManager: Logging { } func removeService(_ loggingService: LoggingService) { - loggingServices.removeAll { $0.serviceIdentifier == loggingService.serviceIdentifier } + loggingServices.removeAll { $0.pluginIdentifier == loggingService.pluginIdentifier } } func log (_ message: StaticString, subsystem: String, category: String, type: OSLogType, _ args: [CVarArg]) { diff --git a/Loop/Managers/LoopAppManager.swift b/Loop/Managers/LoopAppManager.swift index 43c62d128b..0f441651b5 100644 --- a/Loop/Managers/LoopAppManager.swift +++ b/Loop/Managers/LoopAppManager.swift @@ -226,6 +226,7 @@ class LoopAppManager: NSObject { onboardingManager = OnboardingManager(pluginManager: pluginManager, bluetoothProvider: bluetoothStateManager, deviceDataManager: deviceDataManager, + statefulPluginManager: deviceDataManager.statefulPluginManager, servicesManager: deviceDataManager.servicesManager, loopDataManager: deviceDataManager.loopManager, supportManager: supportManager, @@ -238,11 +239,8 @@ class LoopAppManager: NSObject { if let analyticsService = support as? AnalyticsService { analyticsServicesManager.addService(analyticsService) } + support.initializationComplete(for: deviceDataManager.allActivePlugins) } - for support in supportManager.availableSupports { - support.initializationComplete(for: deviceDataManager.servicesManager.activeServices) - } - deviceDataManager.onboardingManager = onboardingManager @@ -254,7 +252,7 @@ class LoopAppManager: NSObject { } analyticsServicesManager.identify("Dosing Strategy", value: settingsManager.loopSettings.automaticDosingStrategy.analyticsValue) - let serviceNames = deviceDataManager.servicesManager.activeServices.map { $0.serviceIdentifier } + let serviceNames = deviceDataManager.servicesManager.activeServices.map { $0.pluginIdentifier } analyticsServicesManager.identify("Services", array: serviceNames) if FeatureFlags.scenariosEnabled { diff --git a/Loop/Managers/OnboardingManager.swift b/Loop/Managers/OnboardingManager.swift index b39e0d7d35..b9f6c8c232 100644 --- a/Loop/Managers/OnboardingManager.swift +++ b/Loop/Managers/OnboardingManager.swift @@ -15,6 +15,7 @@ class OnboardingManager { private let pluginManager: PluginManager private let bluetoothProvider: BluetoothProvider private let deviceDataManager: DeviceDataManager + private let statefulPluginManager: StatefulPluginManager private let servicesManager: ServicesManager private let loopDataManager: LoopDataManager private let supportManager: SupportManager @@ -39,10 +40,20 @@ class OnboardingManager { private var onboardingCompletion: (() -> Void)? - init(pluginManager: PluginManager, bluetoothProvider: BluetoothProvider, deviceDataManager: DeviceDataManager, servicesManager: ServicesManager, loopDataManager: LoopDataManager, supportManager: SupportManager, windowProvider: WindowProvider?, userDefaults: UserDefaults = .standard) { + init(pluginManager: PluginManager, + bluetoothProvider: BluetoothProvider, + deviceDataManager: DeviceDataManager, + statefulPluginManager: StatefulPluginManager, + servicesManager: ServicesManager, + loopDataManager: LoopDataManager, + supportManager: SupportManager, + windowProvider: WindowProvider?, + userDefaults: UserDefaults = .standard) + { self.pluginManager = pluginManager self.bluetoothProvider = bluetoothProvider self.deviceDataManager = deviceDataManager + self.statefulPluginManager = statefulPluginManager self.servicesManager = servicesManager self.loopDataManager = loopDataManager self.supportManager = supportManager @@ -122,7 +133,7 @@ class OnboardingManager { let onboarding = onboardingType.createOnboarding() guard !onboarding.isOnboarded else { - completedOnboardingIdentifiers.append(onboarding.onboardingIdentifier) + completedOnboardingIdentifiers.append(onboarding.pluginIdentifier) continue } @@ -155,7 +166,7 @@ class OnboardingManager { dispatchPrecondition(condition: .onQueue(.main)) if let activeOnboarding = self.activeOnboarding, !isSuspended { - completedOnboardingIdentifiers.append(activeOnboarding.onboardingIdentifier) + completedOnboardingIdentifiers.append(activeOnboarding.pluginIdentifier) self.activeOnboarding = nil } continueOnboarding() @@ -238,25 +249,25 @@ class OnboardingManager { extension OnboardingManager: OnboardingDelegate { func onboardingDidUpdateState(_ onboarding: OnboardingUI) { - guard onboarding.onboardingIdentifier == activeOnboarding?.onboardingIdentifier else { return } + guard onboarding.pluginIdentifier == activeOnboarding?.pluginIdentifier else { return } userDefaults.onboardingManagerActiveOnboardingRawValue = onboarding.rawValue } func onboarding(_ onboarding: OnboardingUI, hasNewTherapySettings therapySettings: TherapySettings) { - guard onboarding.onboardingIdentifier == activeOnboarding?.onboardingIdentifier else { return } + guard onboarding.pluginIdentifier == activeOnboarding?.pluginIdentifier else { return } loopDataManager.therapySettings = therapySettings } func onboarding(_ onboarding: OnboardingUI, hasNewDosingEnabled dosingEnabled: Bool) { - guard onboarding.onboardingIdentifier == activeOnboarding?.onboardingIdentifier else { return } + guard onboarding.pluginIdentifier == activeOnboarding?.pluginIdentifier else { return } loopDataManager.mutateSettings { settings in settings.dosingEnabled = dosingEnabled } } func onboardingDidSuspend(_ onboarding: OnboardingUI) { - log.debug("OnboardingUI %@ did suspend", onboarding.onboardingIdentifier) - guard onboarding.onboardingIdentifier == activeOnboarding?.onboardingIdentifier else { return } + log.debug("OnboardingUI %@ did suspend", onboarding.pluginIdentifier) + guard onboarding.pluginIdentifier == activeOnboarding?.pluginIdentifier else { return } self.isSuspended = true } } @@ -270,7 +281,7 @@ extension OnboardingManager: CompletionDelegate { return } - self.log.debug("completionNotifyingDidComplete during activeOnboarding", activeOnboarding.onboardingIdentifier) + self.log.debug("completionNotifyingDidComplete during activeOnboarding", activeOnboarding.pluginIdentifier) // The `completionNotifyingDidComplete` callback can be called by an onboarding plugin to signal that the user is done with // the onboarding UI, like when pausing, so the onboarding UI can be dismissed. This doesn't necessarily mean that the @@ -340,7 +351,7 @@ extension OnboardingManager: CGMManagerProvider { guard let cgmManager = deviceDataManager.cgmManager else { return deviceDataManager.setupCGMManager(withIdentifier: identifier, prefersToSkipUserInteraction: prefersToSkipUserInteraction) } - guard cgmManager.managerIdentifier == identifier else { + guard cgmManager.pluginIdentifier == identifier else { return .failure(OnboardingError.invalidState) } @@ -384,7 +395,7 @@ extension OnboardingManager: PumpManagerProvider { guard let pumpManager = deviceDataManager.pumpManager else { return deviceDataManager.setupPumpManager(withIdentifier: identifier, initialSettings: settings, prefersToSkipUserInteraction: prefersToSkipUserInteraction) } - guard pumpManager.managerIdentifier == identifier else { + guard pumpManager.pluginIdentifier == identifier else { return .failure(OnboardingError.invalidState) } @@ -396,15 +407,22 @@ extension OnboardingManager: PumpManagerProvider { } } +// MARK: - StatefulPluggableProvider + +extension OnboardingManager: StatefulPluggableProvider { + func statefulPlugin(withIdentifier identifier: String) -> StatefulPluggable? { + statefulPluginManager.statefulPlugin(withIdentifier: identifier) } +} + // MARK: - ServiceProvider -extension OnboardingManager: ServiceProvider { +extension OnboardingManager: ServiceProvider { var activeServices: [Service] { servicesManager.activeServices } var availableServices: [ServiceDescriptor] { servicesManager.availableServices } func onboardService(withIdentifier identifier: String) -> Swift.Result, Error> { - guard let service = activeServices.first(where: { $0.serviceIdentifier == identifier }) else { + guard let service = activeServices.first(where: { $0.pluginIdentifier == identifier }) else { return servicesManager.setupService(withIdentifier: identifier) } @@ -421,6 +439,7 @@ extension OnboardingManager: ServiceProvider { } // MARK: - TherapySettingsProvider + extension OnboardingManager: TherapySettingsProvider { var onboardingTherapySettings: TherapySettings { return loopDataManager.therapySettings @@ -446,7 +465,7 @@ fileprivate extension OnboardingUI { var rawValue: RawValue { return [ - "onboardingIdentifier": onboardingIdentifier, + "onboardingIdentifier": pluginIdentifier, "state": rawState ] } diff --git a/Loop/Managers/RemoteDataServicesManager.swift b/Loop/Managers/RemoteDataServicesManager.swift index 14a3416900..36c460e3c3 100644 --- a/Loop/Managers/RemoteDataServicesManager.swift +++ b/Loop/Managers/RemoteDataServicesManager.swift @@ -64,7 +64,7 @@ final class RemoteDataServicesManager { func removeService(_ remoteDataService: RemoteDataService) { lock.withLock { - unlockedRemoteDataServices.removeAll { $0.serviceIdentifier == remoteDataService.serviceIdentifier } + unlockedRemoteDataServices.removeAll { $0.pluginIdentifier == remoteDataService.pluginIdentifier } } clearQueryAnchors(for: remoteDataService) } @@ -80,7 +80,7 @@ final class RemoteDataServicesManager { private func dispatchQueue(for remoteDataService: RemoteDataService, withRemoteDataType remoteDataType: RemoteDataType) -> DispatchQueue { - let key = UploadTaskKey(serviceIdentifier: remoteDataService.serviceIdentifier, remoteDataType: remoteDataType) + let key = UploadTaskKey(serviceIdentifier: remoteDataService.pluginIdentifier, remoteDataType: remoteDataType) return dispatchQueue(key) } @@ -228,7 +228,7 @@ extension RemoteDataServicesManager { private func uploadAlertData(to remoteDataService: RemoteDataService) { uploadGroup.enter() - let key = UploadTaskKey(serviceIdentifier: remoteDataService.serviceIdentifier, remoteDataType: .alert) + let key = UploadTaskKey(serviceIdentifier: remoteDataService.pluginIdentifier, remoteDataType: .alert) dispatchQueue(key).async { let semaphore = DispatchSemaphore(value: 0) @@ -275,7 +275,7 @@ extension RemoteDataServicesManager { private func uploadCarbData(to remoteDataService: RemoteDataService) { uploadGroup.enter() - let key = UploadTaskKey(serviceIdentifier: remoteDataService.serviceIdentifier, remoteDataType: .carb) + let key = UploadTaskKey(serviceIdentifier: remoteDataService.pluginIdentifier, remoteDataType: .carb) dispatchQueue(key).async { let semaphore = DispatchSemaphore(value: 0) @@ -329,7 +329,7 @@ extension RemoteDataServicesManager { private func uploadDoseData(to remoteDataService: RemoteDataService) { uploadGroup.enter() - let key = UploadTaskKey(serviceIdentifier: remoteDataService.serviceIdentifier, remoteDataType: .dose) + let key = UploadTaskKey(serviceIdentifier: remoteDataService.pluginIdentifier, remoteDataType: .dose) dispatchQueue(key).async { let semaphore = DispatchSemaphore(value: 0) @@ -383,7 +383,7 @@ extension RemoteDataServicesManager { private func uploadDosingDecisionData(to remoteDataService: RemoteDataService) { uploadGroup.enter() - let key = UploadTaskKey(serviceIdentifier: remoteDataService.serviceIdentifier, remoteDataType: .dosingDecision) + let key = UploadTaskKey(serviceIdentifier: remoteDataService.pluginIdentifier, remoteDataType: .dosingDecision) dispatchQueue(key).async { let semaphore = DispatchSemaphore(value: 0) @@ -442,7 +442,7 @@ extension RemoteDataServicesManager { uploadGroup.enter() - let key = UploadTaskKey(serviceIdentifier: remoteDataService.serviceIdentifier, remoteDataType: .glucose) + let key = UploadTaskKey(serviceIdentifier: remoteDataService.pluginIdentifier, remoteDataType: .glucose) dispatchQueue(key).async { let semaphore = DispatchSemaphore(value: 0) @@ -496,7 +496,7 @@ extension RemoteDataServicesManager { private func uploadPumpEventData(to remoteDataService: RemoteDataService) { uploadGroup.enter() - let key = UploadTaskKey(serviceIdentifier: remoteDataService.serviceIdentifier, remoteDataType: .pumpEvent) + let key = UploadTaskKey(serviceIdentifier: remoteDataService.pluginIdentifier, remoteDataType: .pumpEvent) dispatchQueue(for: remoteDataService, withRemoteDataType: .pumpEvent).async { let semaphore = DispatchSemaphore(value: 0) @@ -550,7 +550,7 @@ extension RemoteDataServicesManager { private func uploadSettingsData(to remoteDataService: RemoteDataService) { uploadGroup.enter() - let key = UploadTaskKey(serviceIdentifier: remoteDataService.serviceIdentifier, remoteDataType: .settings) + let key = UploadTaskKey(serviceIdentifier: remoteDataService.pluginIdentifier, remoteDataType: .settings) dispatchQueue(for: remoteDataService, withRemoteDataType: .settings).async { let semaphore = DispatchSemaphore(value: 0) @@ -604,7 +604,7 @@ extension RemoteDataServicesManager { private func uploadTemporaryOverrideData(to remoteDataService: RemoteDataService) { uploadGroup.enter() - let key = UploadTaskKey(serviceIdentifier: remoteDataService.serviceIdentifier, remoteDataType: .overrides) + let key = UploadTaskKey(serviceIdentifier: remoteDataService.pluginIdentifier, remoteDataType: .overrides) dispatchQueue(for: remoteDataService, withRemoteDataType: .overrides).async { let semaphore = DispatchSemaphore(value: 0) @@ -648,7 +648,7 @@ extension RemoteDataServicesManager { func serviceForPushNotification(_ notification: [String: AnyObject]) throws -> RemoteDataService { let defaultServiceIdentifier = "NightscoutService" let serviceIdentifier = notification["serviceIdentifier"] as? String ?? defaultServiceIdentifier - guard let service = remoteDataServices.first(where: {$0.serviceIdentifier == serviceIdentifier}) else { + guard let service = remoteDataServices.first(where: {$0.pluginIdentifier == serviceIdentifier}) else { throw RemoteDataServicesManagerCommandError.unsupportedServiceIdentifier(serviceIdentifier) } return service @@ -674,7 +674,7 @@ protocol RemoteDataServicesManagerDelegate: AnyObject { fileprivate extension UserDefaults { private func queryAnchorKey(for remoteDataService: RemoteDataService, withRemoteDataType remoteDataType: RemoteDataType) -> String { - return "com.loopkit.Loop.RemoteDataServicesManager.\(remoteDataService.serviceIdentifier).\(remoteDataType.rawValue)QueryAnchor" + return "com.loopkit.Loop.RemoteDataServicesManager.\(remoteDataService.pluginIdentifier).\(remoteDataType.rawValue)QueryAnchor" } func getQueryAnchor(for remoteDataService: RemoteDataService, withRemoteDataType remoteDataType: RemoteDataType) -> T? where T: RawRepresentable, T.RawValue == [String: Any] { diff --git a/Loop/Managers/Service.swift b/Loop/Managers/Service.swift index 3966109931..9f4b2f0eee 100644 --- a/Loop/Managers/Service.swift +++ b/Loop/Managers/Service.swift @@ -13,11 +13,11 @@ import MockKit let staticServices: [Service.Type] = [MockService.self] let staticServicesByIdentifier: [String: Service.Type] = staticServices.reduce(into: [:]) { (map, Type) in - map[Type.serviceIdentifier] = Type + map[Type.pluginIdentifier] = Type } let availableStaticServices = staticServices.map { (Type) -> ServiceDescriptor in - return ServiceDescriptor(identifier: Type.serviceIdentifier, localizedTitle: Type.localizedTitle) + return ServiceDescriptor(identifier: Type.pluginIdentifier, localizedTitle: Type.localizedTitle) } func ServiceFromRawValue(_ rawValue: [String: Any]) -> Service? { @@ -30,16 +30,3 @@ func ServiceFromRawValue(_ rawValue: [String: Any]) -> Service? { return ServiceType.init(rawState: rawState) } - -extension Service { - - typealias RawValue = [String: Any] - - var rawValue: RawValue { - return [ - "serviceIdentifier": serviceIdentifier, - "state": rawState - ] - } - -} diff --git a/Loop/Managers/ServicesManager.swift b/Loop/Managers/ServicesManager.swift index 2593560706..1b0ab15b9a 100644 --- a/Loop/Managers/ServicesManager.swift +++ b/Loop/Managers/ServicesManager.swift @@ -124,6 +124,7 @@ class ServicesManager { public func addActiveService(_ service: Service) { servicesLock.withLock { service.serviceDelegate = self + service.stateDelegate = self services.append(service) @@ -153,9 +154,10 @@ class ServicesManager { analyticsServicesManager.removeService(analyticsService) } - services.removeAll { $0.serviceIdentifier == service.serviceIdentifier } + services.removeAll { $0.pluginIdentifier == service.pluginIdentifier } service.serviceDelegate = nil + service.stateDelegate = nil saveState() } @@ -171,6 +173,7 @@ class ServicesManager { rawServices.forEach { rawValue in if let service = serviceFromRawValue(rawValue) { service.serviceDelegate = self + service.stateDelegate = self services.append(service) @@ -238,6 +241,19 @@ public protocol ServicesManagerDelegate: AnyObject { func deliverCarbs(amountInGrams: Double, absorptionTime: TimeInterval?, foodType: String?, startDate: Date?) async throws } +// MARK: - StatefulPluggableDelegate +extension ServicesManager: StatefulPluggableDelegate { + func pluginDidUpdateState(_ plugin: StatefulPluggable) { + saveState() + } + + func pluginWantsDeletion(_ plugin: StatefulPluggable) { + guard let service = plugin as? Service else { return } + log.default("Service with identifier '%{public}@' deleted", service.pluginIdentifier) + removeActiveService(service) + } +} + // MARK: - ServiceDelegate extension ServicesManager: ServiceDelegate { @@ -256,15 +272,6 @@ extension ServicesManager: ServiceDelegate { return semanticVersion } - - func serviceDidUpdateState(_ service: Service) { - saveState() - } - - func serviceWantsDeletion(_ service: Service) { - log.default("Service with identifier '%{public}@' deleted", service.serviceIdentifier) - removeActiveService(service) - } func enactRemoteOverride(name: String, durationTime: TimeInterval?, remoteAddress: String) async throws { @@ -380,13 +387,13 @@ extension ServicesManager: AlertIssuer { extension ServicesManager: ServiceOnboardingDelegate { func serviceOnboarding(didCreateService service: Service) { - log.default("Service with identifier '%{public}@' created", service.serviceIdentifier) + log.default("Service with identifier '%{public}@' created", service.pluginIdentifier) addActiveService(service) } func serviceOnboarding(didOnboardService service: Service) { precondition(service.isOnboarded) - log.default("Service with identifier '%{public}@' onboarded", service.serviceIdentifier) + log.default("Service with identifier '%{public}@' onboarded", service.pluginIdentifier) } } diff --git a/Loop/Managers/StatefulPluggable.swift b/Loop/Managers/StatefulPluggable.swift new file mode 100644 index 0000000000..ab1be4754d --- /dev/null +++ b/Loop/Managers/StatefulPluggable.swift @@ -0,0 +1,20 @@ +// +// StatefulPluggable.swift +// Loop +// +// Created by Nathaniel Hamming on 2023-09-13. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import LoopKit + +extension StatefulPluggable { + typealias RawValue = [String: Any] + + var rawValue: RawValue { + return [ + "statefulPluginIdentifier": pluginIdentifier, + "state": rawState + ] + } +} diff --git a/Loop/Managers/StatefulPluginManager.swift b/Loop/Managers/StatefulPluginManager.swift new file mode 100644 index 0000000000..22fc035b0c --- /dev/null +++ b/Loop/Managers/StatefulPluginManager.swift @@ -0,0 +1,125 @@ +// +// StatefulPluginManager.swift +// Loop +// +// Created by Nathaniel Hamming on 2023-09-06. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import LoopKit +import LoopKitUI +import LoopCore +import Combine + +class StatefulPluginManager: StatefulPluggableProvider { + + private let pluginManager: PluginManager + + private let servicesManager: ServicesManager + + private var statefulPlugins = [StatefulPluggable]() + + private let statefulPluginLock = UnfairLock() + + @PersistedProperty(key: "StatefulPlugins") + var rawStatefulPlugins: [StatefulPluggable.RawStateValue]? + + init(pluginManager: PluginManager, + servicesManager: ServicesManager) + { + self.pluginManager = pluginManager + self.servicesManager = servicesManager + restoreState() + } + + public var availableStatefulPluginIdentifiers: [String] { + return pluginManager.availableStatefulPluginIdentifiers + } + + func statefulPlugin(withIdentifier identifier: String) -> StatefulPluggable? { + for plugin in statefulPlugins { + if plugin.pluginIdentifier == identifier { + return plugin + } + } + + return setupStatefulPlugin(withIdentifier: identifier) + } + + func statefulPluginType(withIdentifier identifier: String) -> StatefulPluggable.Type? { + pluginManager.getStatefulPluginTypeByIdentifier(identifier) + } + + func setupStatefulPlugin(withIdentifier identifier: String) -> StatefulPluggable? { + guard let statefulPluinType = pluginManager.getStatefulPluginTypeByIdentifier(identifier) else { return nil } + + // init without raw value + let statefulPlugin = statefulPluinType.init(rawState: [:]) + statefulPlugin?.initializationComplete(for: servicesManager.activeServices) + addActiveStatefulPlugin(statefulPlugin) + + return statefulPlugin + } + + private func statefulPluginTypeFromRawValue(_ rawValue: StatefulPluggable.RawStateValue) -> StatefulPluggable.Type? { + guard let identifier = rawValue["statefulPluginIdentifier"] as? String else { + return nil + } + + return statefulPluginType(withIdentifier: identifier) + } + + private func statefulPluginFromRawValue(_ rawValue: StatefulPluggable.RawStateValue) -> StatefulPluggable? { + guard let statefulPluginType = statefulPluginTypeFromRawValue(rawValue), + let rawState = rawValue["state"] as? StatefulPluggable.RawStateValue + else { + return nil + } + + return statefulPluginType.init(rawState: rawState) + } + + public var activeStatefulPlugins: [StatefulPluggable] { + return statefulPluginLock.withLock { statefulPlugins } + } + + public func addActiveStatefulPlugin(_ statefulPlugin: StatefulPluggable?) { + guard let statefulPlugin = statefulPlugin else { return } + statefulPluginLock.withLock { + statefulPlugin.stateDelegate = self + statefulPlugins.append(statefulPlugin) + saveState() + } + } + + public func removeActiveStatefulPlugin(_ statefulPlugin: StatefulPluggable) { + statefulPluginLock.withLock { + statefulPlugins.removeAll { $0.pluginIdentifier == statefulPlugin.pluginIdentifier } + saveState() + } + } + + private func saveState() { + rawStatefulPlugins = statefulPlugins.compactMap { $0.rawValue } + } + + private func restoreState() { + let rawStatefulPlugins = rawStatefulPlugins ?? [] + rawStatefulPlugins.forEach { rawValue in + if let statefulPlugin = statefulPluginFromRawValue(rawValue) { + statefulPlugin.initializationComplete(for: servicesManager.activeServices) + statefulPlugins.append(statefulPlugin) + } + } + } +} + +extension StatefulPluginManager: StatefulPluggableDelegate { + func pluginDidUpdateState(_ plugin: StatefulPluggable) { + saveState() + } + + func pluginWantsDeletion(_ plugin: LoopKit.StatefulPluggable) { + removeActiveStatefulPlugin(plugin) + } +} diff --git a/Loop/Managers/SupportManager.swift b/Loop/Managers/SupportManager.swift index dae07c7e25..58cddddf74 100644 --- a/Loop/Managers/SupportManager.swift +++ b/Loop/Managers/SupportManager.swift @@ -54,7 +54,7 @@ public final class SupportManager { self.pluginManager = pluginManager self.staticSupportTypes = [] staticSupportTypesByIdentifier = self.staticSupportTypes.reduce(into: [:]) { (map, type) in - map[type.supportIdentifier] = type + map[type.pluginIdentifier] = type } restoreState() @@ -75,7 +75,7 @@ public final class SupportManager { for bundle in remainingSupportBundles { do { if let support = try bundle.loadAndInstantiateSupport() { - log.debug("Loaded support plugin: %{public}@", support.identifier) + log.debug("Loaded support plugin: %{public}@", support.pluginIdentifier) addSupport(support) } } catch { @@ -111,8 +111,8 @@ public final class SupportManager { extension SupportManager { func addSupport(_ support: SupportUI) { supports.mutate { - if $0[support.identifier] == nil { - $0[support.identifier] = support + if $0[support.pluginIdentifier] == nil { + $0[support.pluginIdentifier] = support support.delegate = self } } @@ -124,7 +124,7 @@ extension SupportManager { func removeSupport(_ support: SupportUI) { supports.mutate { - $0[support.identifier] = nil + $0[support.pluginIdentifier] = nil support.delegate = self } } @@ -156,7 +156,7 @@ extension SupportManager { supports.value.values.forEach { support in group.addTask { - return (await support.checkVersion(bundleIdentifier: Bundle.main.bundleIdentifier!, currentVersion: Bundle.main.shortVersionString), support.identifier) + return (await support.checkVersion(bundleIdentifier: Bundle.main.bundleIdentifier!, currentVersion: Bundle.main.shortVersionString), support.pluginIdentifier) } } @@ -331,7 +331,7 @@ fileprivate extension UserDefaults { extension SupportUI { var rawValue: RawStateValue { return [ - "supportIdentifier": Self.supportIdentifier, + "supportIdentifier": Self.pluginIdentifier, "state": rawState ] } diff --git a/Loop/Managers/TestingScenariosManager.swift b/Loop/Managers/TestingScenariosManager.swift index eabf1b060a..b71e357433 100644 --- a/Loop/Managers/TestingScenariosManager.swift +++ b/Loop/Managers/TestingScenariosManager.swift @@ -199,7 +199,7 @@ extension TestingScenariosManagerRequirements { if instance.hasCGMData { if let cgmManager = deviceManager.cgmManager as? TestingCGMManager { if instance.shouldReloadManager?.cgm == true { - testingCGMManager = reloadCGMManager(withIdentifier: cgmManager.managerIdentifier) + testingCGMManager = reloadCGMManager(withIdentifier: cgmManager.pluginIdentifier) } else { testingCGMManager = cgmManager } @@ -212,7 +212,7 @@ extension TestingScenariosManagerRequirements { if instance.hasPumpData { if let pumpManager = deviceManager.pumpManager as? TestingPumpManager { if instance.shouldReloadManager?.pump == true { - testingPumpManager = reloadPumpManager(withIdentifier: pumpManager.managerIdentifier) + testingPumpManager = reloadPumpManager(withIdentifier: pumpManager.pluginIdentifier) } else { testingPumpManager = pumpManager } @@ -243,9 +243,9 @@ extension TestingScenariosManagerRequirements { } instance.deviceActions.forEach { [testingCGMManager, testingPumpManager] action in - if testingCGMManager?.managerIdentifier == action.managerIdentifier { + if testingCGMManager?.pluginIdentifier == action.managerIdentifier { testingCGMManager?.trigger(action: action) - } else if testingPumpManager?.managerIdentifier == action.managerIdentifier { + } else if testingPumpManager?.pluginIdentifier == action.managerIdentifier { testingPumpManager?.trigger(action: action) } } diff --git a/Loop/Plugins/PluginManager.swift b/Loop/Plugins/PluginManager.swift index bec19e0602..a254d26872 100644 --- a/Loop/Plugins/PluginManager.swift +++ b/Loop/Plugins/PluginManager.swift @@ -145,6 +145,37 @@ class PluginManager { return ServiceDescriptor(identifier: identifier, localizedTitle: title) }) } + + func getStatefulPluginTypeByIdentifier(_ identifier: String) -> StatefulPluggable.Type? { + for bundle in pluginBundles { + if let name = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.statefulPluginIdentifier.rawValue) as? String, name == identifier { + do { + try bundle.loadAndReturnError() + + if let principalClass = bundle.principalClass as? NSObject.Type { + + if let plugin = principalClass.init() as? StatefulPlugin { + return plugin.pluginType + } else { + fatalError("PrincipalClass does not conform to StatefulPlugin") + } + + } else { + fatalError("PrincipalClass not found") + } + } catch let error { + log.error("Error loading plugin: %{public}@", String(describing: error)) + } + } + } + return nil + } + + var availableStatefulPluginIdentifiers: [String] { + return pluginBundles.compactMap({ (bundle) -> String? in + return bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.statefulPluginIdentifier.rawValue) as? String + }) + } func getOnboardingTypeByIdentifier(_ identifier: String) -> OnboardingUI.Type? { for bundle in pluginBundles { @@ -201,18 +232,18 @@ class PluginManager { } return nil } - } extension Bundle { var isPumpManagerPlugin: Bool { object(forInfoDictionaryKey: LoopPluginBundleKey.pumpManagerIdentifier.rawValue) as? String != nil } var isCGMManagerPlugin: Bool { object(forInfoDictionaryKey: LoopPluginBundleKey.cgmManagerIdentifier.rawValue) as? String != nil } + var isStatefulPlugin: Bool { object(forInfoDictionaryKey: LoopPluginBundleKey.statefulPluginIdentifier.rawValue) as? String != nil } var isServicePlugin: Bool { object(forInfoDictionaryKey: LoopPluginBundleKey.serviceIdentifier.rawValue) as? String != nil } var isOnboardingPlugin: Bool { object(forInfoDictionaryKey: LoopPluginBundleKey.onboardingIdentifier.rawValue) as? String != nil } var isSupportPlugin: Bool { object(forInfoDictionaryKey: LoopPluginBundleKey.supportIdentifier.rawValue) as? String != nil } - var isLoopPlugin: Bool { isPumpManagerPlugin || isCGMManagerPlugin || isServicePlugin || isOnboardingPlugin || isSupportPlugin } + var isLoopPlugin: Bool { isPumpManagerPlugin || isCGMManagerPlugin || isStatefulPlugin || isServicePlugin || isOnboardingPlugin || isSupportPlugin } var isLoopExtension: Bool { object(forInfoDictionaryKey: LoopPluginBundleKey.extensionIdentifier.rawValue) as? String != nil } diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index 3e5312dafb..8906a75986 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -2255,7 +2255,7 @@ extension StatusTableViewController: ServicesViewModelDelegate { } func gotoService(withIdentifier identifier: String) { - guard let serviceUI = deviceManager.servicesManager.activeServices.first(where: { $0.serviceIdentifier == identifier }) as? ServiceUI else { + guard let serviceUI = deviceManager.servicesManager.activeServices.first(where: { $0.pluginIdentifier == identifier }) as? ServiceUI else { return } showServiceSettings(serviceUI) diff --git a/Loop/View Models/ServicesViewModel.swift b/Loop/View Models/ServicesViewModel.swift index d59e1e6603..19fb2a7d57 100644 --- a/Loop/View Models/ServicesViewModel.swift +++ b/Loop/View Models/ServicesViewModel.swift @@ -24,7 +24,7 @@ public class ServicesViewModel: ObservableObject { var inactiveServices: () -> [ServiceDescriptor] { return { return self.availableServices().filter { availableService in - !self.activeServices().contains { $0.serviceIdentifier == availableService.identifier } + !self.activeServices().contains { $0.pluginIdentifier == availableService.identifier } } } } @@ -42,7 +42,7 @@ public class ServicesViewModel: ObservableObject { } func didTapService(_ index: Int) { - delegate?.gotoService(withIdentifier: activeServices()[index].serviceIdentifier) + delegate?.gotoService(withIdentifier: activeServices()[index].pluginIdentifier) } func didTapAddService(_ availableService: ServiceDescriptor) { @@ -54,23 +54,25 @@ public class ServicesViewModel: ObservableObject { extension ServicesViewModel { fileprivate class FakeService1: Service { static var localizedTitle: String = "Service 1" - static var serviceIdentifier: String = "FakeService1" + static var pluginIdentifier: String = "FakeService1" + var stateDelegate: StatefulPluggableDelegate? var serviceDelegate: ServiceDelegate? var rawState: RawStateValue = [:] required init() {} required init?(rawState: RawStateValue) {} let isOnboarded = true - var available: ServiceDescriptor { ServiceDescriptor(identifier: serviceIdentifier, localizedTitle: localizedTitle) } + var available: ServiceDescriptor { ServiceDescriptor(identifier: pluginIdentifier, localizedTitle: localizedTitle) } } fileprivate class FakeService2: Service { static var localizedTitle: String = "Service 2" - static var serviceIdentifier: String = "FakeService2" + static var pluginIdentifier: String = "FakeService2" + var stateDelegate: StatefulPluggableDelegate? var serviceDelegate: ServiceDelegate? var rawState: RawStateValue = [:] required init() {} required init?(rawState: RawStateValue) {} let isOnboarded = true - var available: ServiceDescriptor { ServiceDescriptor(identifier: serviceIdentifier, localizedTitle: localizedTitle) } + var available: ServiceDescriptor { ServiceDescriptor(identifier: pluginIdentifier, localizedTitle: localizedTitle) } } static var preview: ServicesViewModel { diff --git a/Loop/Views/SettingsView.swift b/Loop/Views/SettingsView.swift index b8e9c1daf7..795b544b2b 100644 --- a/Loop/Views/SettingsView.swift +++ b/Loop/Views/SettingsView.swift @@ -308,7 +308,7 @@ extension SettingsView { private var pluginMenuItems: [PluginMenuItem] { self.viewModel.availableSupports.flatMap { plugin in plugin.configurationMenuItems().enumerated().map { index, item in - PluginMenuItem(section: item.section, view: item.view, pluginIdentifier: plugin.identifier, offset: index) + PluginMenuItem(section: item.section, view: item.view, pluginIdentifier: plugin.pluginIdentifier, offset: index) } } } diff --git a/LoopTests/Managers/DoseEnactorTests.swift b/LoopTests/Managers/DoseEnactorTests.swift index 72359793e6..bf722ec874 100644 --- a/LoopTests/Managers/DoseEnactorTests.swift +++ b/LoopTests/Managers/DoseEnactorTests.swift @@ -121,7 +121,7 @@ class MockPumpManager: PumpManager { .minutes(units / deliveryUnitsPerMinute) } - var managerIdentifier: String = "MockPumpManager" + static var pluginIdentifier: String = "MockPumpManager" var localizedTitle: String = "MockPumpManager" diff --git a/LoopTests/Managers/SupportManagerTests.swift b/LoopTests/Managers/SupportManagerTests.swift index ac0d42b512..48fa42e4d8 100644 --- a/LoopTests/Managers/SupportManagerTests.swift +++ b/LoopTests/Managers/SupportManagerTests.swift @@ -34,7 +34,7 @@ class SupportManagerTests: XCTestCase { weak var delegate: SupportUIDelegate? } class MockSupport: Mixin, SupportUI { - static var supportIdentifier: String { "SupportManagerTestsMockSupport" } + static var pluginIdentifier: String { "SupportManagerTestsMockSupport" } override init() { super.init() } required init?(rawState: RawStateValue) { super.init() } var rawState: RawStateValue = [:] @@ -42,12 +42,11 @@ class SupportManagerTests: XCTestCase { func getScenarios(from scenarioURLs: [URL]) -> [LoopScenario] { [] } func loopWillReset() {} func loopDidReset() {} - func initializationComplete(for services: [LoopKit.Service]) {} func configurationMenuItems() -> [LoopKitUI.CustomMenuItem] { return [] } } class AnotherMockSupport: Mixin, SupportUI { - static var supportIdentifier: String { "SupportManagerTestsAnotherMockSupport" } + static var pluginIdentifier: String { "SupportManagerTestsAnotherMockSupport" } override init() { super.init() } required init?(rawState: RawStateValue) { super.init() } var rawState: RawStateValue = [:] @@ -55,7 +54,6 @@ class SupportManagerTests: XCTestCase { func getScenarios(from scenarioURLs: [URL]) -> [LoopScenario] { [] } func loopWillReset() {} func loopDidReset() {} - func initializationComplete(for services: [LoopKit.Service]) {} func configurationMenuItems() -> [LoopKitUI.CustomMenuItem] { return [] } } From 49b329e3a4199fff07df7aa21da95182ac663c1b Mon Sep 17 00:00:00 2001 From: Nathaniel Hamming Date: Thu, 21 Sep 2023 02:53:24 -0300 Subject: [PATCH 31/49] [COASTAL-1291] corrected rawValue key for service restore (#598) --- Loop/Managers/Service.swift | 2 +- Loop/Managers/ServicesManager.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Loop/Managers/Service.swift b/Loop/Managers/Service.swift index 9f4b2f0eee..1541208712 100644 --- a/Loop/Managers/Service.swift +++ b/Loop/Managers/Service.swift @@ -21,7 +21,7 @@ let availableStaticServices = staticServices.map { (Type) -> ServiceDescriptor i } func ServiceFromRawValue(_ rawValue: [String: Any]) -> Service? { - guard let serviceIdentifier = rawValue["serviceIdentifier"] as? String, + guard let serviceIdentifier = rawValue["statefulPluginIdentifier"] as? String, let rawState = rawValue["state"] as? Service.RawStateValue, let ServiceType = staticServicesByIdentifier[serviceIdentifier] else { diff --git a/Loop/Managers/ServicesManager.swift b/Loop/Managers/ServicesManager.swift index 1b0ab15b9a..7e62e95333 100644 --- a/Loop/Managers/ServicesManager.swift +++ b/Loop/Managers/ServicesManager.swift @@ -100,7 +100,7 @@ class ServicesManager { } private func serviceTypeFromRawValue(_ rawValue: Service.RawStateValue) -> Service.Type? { - guard let identifier = rawValue["serviceIdentifier"] as? String else { + guard let identifier = rawValue["statefulPluginIdentifier"] as? String else { return nil } From bea91f06dbf6736e160f31329fb003d063337da8 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Fri, 22 Sep 2023 08:15:07 -0500 Subject: [PATCH 32/49] Adding CGM Event Store (#2071) --- Loop/Managers/DeviceDataManager.swift | 54 ++++--- Loop/Managers/LoopAppManager.swift | 2 +- Loop/Managers/RemoteDataServicesManager.swift | 152 ++++++------------ Loop/Managers/SettingsManager.swift | 2 +- Scripts/capture-build-details.sh | 3 +- 5 files changed, 88 insertions(+), 125 deletions(-) diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 2e8d157531..95ef432094 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -157,6 +157,8 @@ final class DeviceDataManager { let glucoseStore: GlucoseStore + let cgmEventStore: CgmEventStore + private let cacheStore: PersistenceController let dosingDecisionStore: DosingDecisionStore @@ -337,11 +339,13 @@ final class DeviceDataManager { cgmStalenessMonitor = CGMStalenessMonitor() cgmStalenessMonitor.delegate = glucoseStore + + cgmEventStore = CgmEventStore(cacheStore: cacheStore, cacheLength: localCacheDuration) + + dosingDecisionStore = DosingDecisionStore(store: cacheStore, expireAfter: localCacheDuration) - self.dosingDecisionStore = DosingDecisionStore(store: cacheStore, expireAfter: localCacheDuration) - - self.cgmHasValidSensorSession = false - self.pumpIsAllowingAutomation = true + cgmHasValidSensorSession = false + pumpIsAllowingAutomation = true self.automaticDosingStatus = automaticDosingStatus // HealthStorePreferredGlucoseUnitDidChange will be notified once the user completes the health access form. Set to .milligramsPerDeciliter until then @@ -405,6 +409,7 @@ final class DeviceDataManager { doseStore: doseStore, dosingDecisionStore: dosingDecisionStore, glucoseStore: glucoseStore, + cgmEventStore: cgmEventStore, settingsStore: settingsManager.settingsStore, overrideHistory: overrideHistory, insulinDeliveryStore: doseStore.insulinDeliveryStore @@ -435,6 +440,7 @@ final class DeviceDataManager { doseStore.delegate = self dosingDecisionStore.delegate = self glucoseStore.delegate = self + cgmEventStore.delegate = self doseStore.insulinDeliveryStore.delegate = self remoteDataServicesManager.delegate = self @@ -934,6 +940,16 @@ extension DeviceDataManager: CGMManagerDelegate { } } + func cgmManager(_ manager: LoopKit.CGMManager, hasNew events: [PersistedCgmEvent]) { + Task { + do { + try await cgmEventStore.add(events: events) + } catch { + self.log.error("Error storing cgm events: %{public}@", error.localizedDescription) + } + } + } + func startDateToFilterNewData(for manager: CGMManager) -> Date? { dispatchPrecondition(condition: .onQueue(queue)) return glucoseStore.latestGlucose?.startDate @@ -1191,60 +1207,56 @@ extension DeviceDataManager: PumpManagerOnboardingDelegate { // MARK: - AlertStoreDelegate extension DeviceDataManager: AlertStoreDelegate { - func alertStoreHasUpdatedAlertData(_ alertStore: AlertStore) { - remoteDataServicesManager.alertStoreHasUpdatedAlertData(alertStore) + remoteDataServicesManager.triggerUpload(for: .alert) } - } // MARK: - CarbStoreDelegate extension DeviceDataManager: CarbStoreDelegate { - func carbStoreHasUpdatedCarbData(_ carbStore: CarbStore) { - remoteDataServicesManager.carbStoreHasUpdatedCarbData(carbStore) + remoteDataServicesManager.triggerUpload(for: .carb) } func carbStore(_ carbStore: CarbStore, didError error: CarbStore.CarbStoreError) {} - } // MARK: - DoseStoreDelegate extension DeviceDataManager: DoseStoreDelegate { - func doseStoreHasUpdatedPumpEventData(_ doseStore: DoseStore) { - remoteDataServicesManager.doseStoreHasUpdatedPumpEventData(doseStore) + remoteDataServicesManager.triggerUpload(for: .pumpEvent) } - } // MARK: - DosingDecisionStoreDelegate extension DeviceDataManager: DosingDecisionStoreDelegate { - func dosingDecisionStoreHasUpdatedDosingDecisionData(_ dosingDecisionStore: DosingDecisionStore) { - remoteDataServicesManager.dosingDecisionStoreHasUpdatedDosingDecisionData(dosingDecisionStore) + remoteDataServicesManager.triggerUpload(for: .dosingDecision) } - } // MARK: - GlucoseStoreDelegate extension DeviceDataManager: GlucoseStoreDelegate { - func glucoseStoreHasUpdatedGlucoseData(_ glucoseStore: GlucoseStore) { - remoteDataServicesManager.glucoseStoreHasUpdatedGlucoseData(glucoseStore) + remoteDataServicesManager.triggerUpload(for: .glucose) } - } // MARK: - InsulinDeliveryStoreDelegate extension DeviceDataManager: InsulinDeliveryStoreDelegate { - func insulinDeliveryStoreHasUpdatedDoseData(_ insulinDeliveryStore: InsulinDeliveryStore) { - remoteDataServicesManager.insulinDeliveryStoreHasUpdatedDoseData(insulinDeliveryStore) + remoteDataServicesManager.triggerUpload(for: .dose) } +} +// MARK: - CgmEventStoreDelegate +extension DeviceDataManager: CgmEventStoreDelegate { + func cgmEventStoreHasUpdatedData(_ cgmEventStore: LoopKit.CgmEventStore) { + remoteDataServicesManager.triggerUpload(for: .cgmEvent) + } } + // MARK: - TestingPumpManager extension DeviceDataManager { func deleteTestingPumpData(completion: ((Error?) -> Void)? = nil) { diff --git a/Loop/Managers/LoopAppManager.swift b/Loop/Managers/LoopAppManager.swift index 9edf481ba2..95a7ed6d2d 100644 --- a/Loop/Managers/LoopAppManager.swift +++ b/Loop/Managers/LoopAppManager.swift @@ -603,7 +603,7 @@ extension LoopAppManager: TemporaryScheduleOverrideHistoryDelegate { func temporaryScheduleOverrideHistoryDidUpdate(_ history: TemporaryScheduleOverrideHistory) { UserDefaults.appGroup?.overrideHistory = history - deviceDataManager.remoteDataServicesManager.temporaryScheduleOverrideHistoryDidUpdate() + deviceDataManager.remoteDataServicesManager.triggerUpload(for: .overrides) } } diff --git a/Loop/Managers/RemoteDataServicesManager.swift b/Loop/Managers/RemoteDataServicesManager.swift index 14a3416900..3b86cf7f1e 100644 --- a/Loop/Managers/RemoteDataServicesManager.swift +++ b/Loop/Managers/RemoteDataServicesManager.swift @@ -10,13 +10,14 @@ import os.log import Foundation import LoopKit -enum RemoteDataType: String { +enum RemoteDataType: String, CaseIterable { case alert = "Alert" case carb = "Carb" case dose = "Dose" case dosingDecision = "DosingDecision" case glucose = "Glucose" case pumpEvent = "PumpEvent" + case cgmEvent = "CgmEvent" case settings = "Settings" case overrides = "Overrides" @@ -129,6 +130,8 @@ final class RemoteDataServicesManager { private let glucoseStore: GlucoseStore + private let cgmEventStore: CgmEventStore + private let insulinDeliveryStore: InsulinDeliveryStore private let settingsStore: SettingsStore @@ -141,6 +144,7 @@ final class RemoteDataServicesManager { doseStore: DoseStore, dosingDecisionStore: DosingDecisionStore, glucoseStore: GlucoseStore, + cgmEventStore: CgmEventStore, settingsStore: SettingsStore, overrideHistory: TemporaryScheduleOverrideHistory, insulinDeliveryStore: InsulinDeliveryStore @@ -150,6 +154,7 @@ final class RemoteDataServicesManager { self.doseStore = doseStore self.dosingDecisionStore = dosingDecisionStore self.glucoseStore = glucoseStore + self.cgmEventStore = cgmEventStore self.insulinDeliveryStore = insulinDeliveryStore self.settingsStore = settingsStore self.overrideHistory = overrideHistory @@ -167,13 +172,11 @@ final class RemoteDataServicesManager { } private func clearQueryAnchors(for remoteDataService: RemoteDataService) { - clearAlertQueryAnchor(for: remoteDataService) - clearCarbQueryAnchor(for: remoteDataService) - clearDoseQueryAnchor(for: remoteDataService) - clearDosingDecisionQueryAnchor(for: remoteDataService) - clearGlucoseQueryAnchor(for: remoteDataService) - clearPumpEventQueryAnchor(for: remoteDataService) - clearSettingsQueryAnchor(for: remoteDataService) + for remoteDataType in RemoteDataType.allCases { + dispatchQueue(for: remoteDataService, withRemoteDataType: remoteDataType).async { + UserDefaults.appGroup?.deleteQueryAnchor(for: remoteDataService, withRemoteDataType: remoteDataType) + } + } } func triggerUpload(for triggeringType: RemoteDataType) { @@ -195,6 +198,8 @@ final class RemoteDataServicesManager { remoteDataServices.forEach { self.uploadGlucoseData(to: $0) } case .pumpEvent: remoteDataServices.forEach { self.uploadPumpEventData(to: $0) } + case .cgmEvent: + remoteDataServices.forEach { self.uploadCgmEventData(to: $0) } case .settings: remoteDataServices.forEach { self.uploadSettingsData(to: $0) } case .overrides: @@ -220,11 +225,6 @@ final class RemoteDataServicesManager { } extension RemoteDataServicesManager { - - public func alertStoreHasUpdatedAlertData(_ alertStore: AlertStore) { - triggerUpload(for: .alert) - } - private func uploadAlertData(to remoteDataService: RemoteDataService) { uploadGroup.enter() @@ -257,21 +257,9 @@ extension RemoteDataServicesManager { self.uploadGroup.leave() } } - - private func clearAlertQueryAnchor(for remoteDataService: RemoteDataService) { - dispatchQueue(for: remoteDataService, withRemoteDataType: .alert).async { - UserDefaults.appGroup?.deleteQueryAnchor(for: remoteDataService, withRemoteDataType: .alert) - } - } - } extension RemoteDataServicesManager { - - public func carbStoreHasUpdatedCarbData(_ carbStore: CarbStore) { - triggerUpload(for: .carb) - } - private func uploadCarbData(to remoteDataService: RemoteDataService) { uploadGroup.enter() @@ -311,21 +299,9 @@ extension RemoteDataServicesManager { } } } - - private func clearCarbQueryAnchor(for remoteDataService: RemoteDataService) { - dispatchQueue(for: remoteDataService, withRemoteDataType: .carb).async { - UserDefaults.appGroup?.deleteQueryAnchor(for: remoteDataService, withRemoteDataType: .carb) - } - } - } extension RemoteDataServicesManager { - - public func insulinDeliveryStoreHasUpdatedDoseData(_ insulinDeliveryStore: InsulinDeliveryStore) { - triggerUpload(for: .dose) - } - private func uploadDoseData(to remoteDataService: RemoteDataService) { uploadGroup.enter() @@ -365,21 +341,9 @@ extension RemoteDataServicesManager { } } } - - private func clearDoseQueryAnchor(for remoteDataService: RemoteDataService) { - dispatchQueue(for: remoteDataService, withRemoteDataType: .dose).async { - UserDefaults.appGroup?.deleteQueryAnchor(for: remoteDataService, withRemoteDataType: .dose) - } - } - } extension RemoteDataServicesManager { - - public func dosingDecisionStoreHasUpdatedDosingDecisionData(_ dosingDecisionStore: DosingDecisionStore) { - triggerUpload(for: .dosingDecision) - } - private func uploadDosingDecisionData(to remoteDataService: RemoteDataService) { uploadGroup.enter() @@ -419,21 +383,9 @@ extension RemoteDataServicesManager { } } } - - private func clearDosingDecisionQueryAnchor(for remoteDataService: RemoteDataService) { - dispatchQueue(for: remoteDataService, withRemoteDataType: .dosingDecision).async { - UserDefaults.appGroup?.deleteQueryAnchor(for: remoteDataService, withRemoteDataType: .dosingDecision) - } - } - } extension RemoteDataServicesManager { - - public func glucoseStoreHasUpdatedGlucoseData(_ glucoseStore: GlucoseStore) { - triggerUpload(for: .glucose) - } - private func uploadGlucoseData(to remoteDataService: RemoteDataService) { if delegate?.shouldSyncToRemoteService == false { @@ -478,21 +430,9 @@ extension RemoteDataServicesManager { } } } - - private func clearGlucoseQueryAnchor(for remoteDataService: RemoteDataService) { - dispatchQueue(for: remoteDataService, withRemoteDataType: .glucose).async { - UserDefaults.appGroup?.deleteQueryAnchor(for: remoteDataService, withRemoteDataType: .glucose) - } - } - } extension RemoteDataServicesManager { - - public func doseStoreHasUpdatedPumpEventData(_ doseStore: DoseStore) { - triggerUpload(for: .pumpEvent) - } - private func uploadPumpEventData(to remoteDataService: RemoteDataService) { uploadGroup.enter() @@ -532,21 +472,9 @@ extension RemoteDataServicesManager { } } } - - private func clearPumpEventQueryAnchor(for remoteDataService: RemoteDataService) { - dispatchQueue(for: remoteDataService, withRemoteDataType: .pumpEvent).async { - UserDefaults.appGroup?.deleteQueryAnchor(for: remoteDataService, withRemoteDataType: .pumpEvent) - } - } - } extension RemoteDataServicesManager { - - public func settingsStoreHasUpdatedSettingsData(_ settingsStore: SettingsStore) { - triggerUpload(for: .settings) - } - private func uploadSettingsData(to remoteDataService: RemoteDataService) { uploadGroup.enter() @@ -586,21 +514,9 @@ extension RemoteDataServicesManager { } } } - - private func clearSettingsQueryAnchor(for remoteDataService: RemoteDataService) { - dispatchQueue(for: remoteDataService, withRemoteDataType: .settings).async { - UserDefaults.appGroup?.deleteQueryAnchor(for: remoteDataService, withRemoteDataType: .settings) - } - } - } extension RemoteDataServicesManager { - - public func temporaryScheduleOverrideHistoryDidUpdate() { - triggerUpload(for: .overrides) - } - private func uploadTemporaryOverrideData(to remoteDataService: RemoteDataService) { uploadGroup.enter() @@ -629,10 +545,46 @@ extension RemoteDataServicesManager { self.uploadGroup.leave() } } +} - private func clearTemporaryOverrideQueryAnchor(for remoteDataService: RemoteDataService) { - dispatchQueue(for: remoteDataService, withRemoteDataType: .overrides).async { - UserDefaults.appGroup?.deleteQueryAnchor(for: remoteDataService, withRemoteDataType: .overrides) +extension RemoteDataServicesManager { + private func uploadCgmEventData(to remoteDataService: RemoteDataService) { + uploadGroup.enter() + + let key = UploadTaskKey(serviceIdentifier: remoteDataService.serviceIdentifier, remoteDataType: .pumpEvent) + + dispatchQueue(for: remoteDataService, withRemoteDataType: .cgmEvent).async { + let semaphore = DispatchSemaphore(value: 0) + let previousQueryAnchor = UserDefaults.appGroup?.getQueryAnchor(for: remoteDataService, withRemoteDataType: .cgmEvent) ?? CgmEventStore.QueryAnchor() + var continueUpload = false + + self.cgmEventStore.executeCgmEventQuery(fromQueryAnchor: previousQueryAnchor) { result in + switch result { + case .failure(let error): + self.log.error("Error querying pump event data: %{public}@", String(describing: error)) + semaphore.signal() + case .success(let queryAnchor, let data): + remoteDataService.uploadCgmEventData(data) { result in + switch result { + case .failure(let error): + self.log.error("Error synchronizing pump event data: %{public}@", String(describing: error)) + self.uploadFailed(key) + case .success: + UserDefaults.appGroup?.setQueryAnchor(for: remoteDataService, withRemoteDataType: .cgmEvent, queryAnchor) + continueUpload = queryAnchor != previousQueryAnchor + self.uploadSucceeded(key) + } + semaphore.signal() + } + } + } + + semaphore.wait() + self.uploadGroup.leave() + + if continueUpload { + self.uploadPumpEventData(to: remoteDataService) + } } } } diff --git a/Loop/Managers/SettingsManager.swift b/Loop/Managers/SettingsManager.swift index f7421b97f7..e3fdb60bf7 100644 --- a/Loop/Managers/SettingsManager.swift +++ b/Loop/Managers/SettingsManager.swift @@ -211,7 +211,7 @@ class SettingsManager { // MARK: - SettingsStoreDelegate extension SettingsManager: SettingsStoreDelegate { func settingsStoreHasUpdatedSettingsData(_ settingsStore: SettingsStore) { - remoteDataServicesManager?.settingsStoreHasUpdatedSettingsData(settingsStore) + remoteDataServicesManager?.triggerUpload(for: .settings) } } diff --git a/Scripts/capture-build-details.sh b/Scripts/capture-build-details.sh index 66f827d7c3..6122592374 100755 --- a/Scripts/capture-build-details.sh +++ b/Scripts/capture-build-details.sh @@ -10,10 +10,9 @@ SCRIPT_DIRECTORY="$(dirname "${0}")" error() { echo "ERROR: ${*}" >&2 - echo "Usage: ${SCRIPT} [-r|--git-source-root git-source-root] [-p|--provisioning-profile-path provisioning-profile-path] [-i|--info-plist-path info-plist-path]" >&2 + echo "Usage: ${SCRIPT} [-r|--git-source-root git-source-root] [-p|--provisioning-profile-path provisioning-profile-path]" >&2 echo "Parameters:" >&2 echo " -p|--provisioning-profile-path path to the .mobileprovision provisioning profile file to check for expiration; optional, defaults to \${HOME}/Library/MobileDevice/Provisioning Profiles/\${EXPANDED_PROVISIONING_PROFILE}.mobileprovision" >&2 - echo " -i|--info-plist-path path to the Info.plist file to modify; optional, defaults to \${BUILT_PRODUCTS_DIR}/\${INFOPLIST_PATH}" >&2 exit 1 } From c7091c00bb8a9412c881dcf49d30013b121a34dd Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Fri, 22 Sep 2023 15:36:31 -0500 Subject: [PATCH 33/49] Use pluginIdentifier for upload key --- Loop/Managers/RemoteDataServicesManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Loop/Managers/RemoteDataServicesManager.swift b/Loop/Managers/RemoteDataServicesManager.swift index c256013ba0..296e3befa9 100644 --- a/Loop/Managers/RemoteDataServicesManager.swift +++ b/Loop/Managers/RemoteDataServicesManager.swift @@ -551,7 +551,7 @@ extension RemoteDataServicesManager { private func uploadCgmEventData(to remoteDataService: RemoteDataService) { uploadGroup.enter() - let key = UploadTaskKey(serviceIdentifier: remoteDataService.serviceIdentifier, remoteDataType: .pumpEvent) + let key = UploadTaskKey(serviceIdentifier: remoteDataService.pluginIdentifier, remoteDataType: .pumpEvent) dispatchQueue(for: remoteDataService, withRemoteDataType: .cgmEvent).async { let semaphore = DispatchSemaphore(value: 0) From 15f05acc362b1193c053765f2fc33793bded744d Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Sat, 23 Sep 2023 10:07:09 -0500 Subject: [PATCH 34/49] Revert rawValue key name change for services --- Loop/Managers/Service.swift | 2 +- Loop/Managers/ServicesManager.swift | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Loop/Managers/Service.swift b/Loop/Managers/Service.swift index 1541208712..9f4b2f0eee 100644 --- a/Loop/Managers/Service.swift +++ b/Loop/Managers/Service.swift @@ -21,7 +21,7 @@ let availableStaticServices = staticServices.map { (Type) -> ServiceDescriptor i } func ServiceFromRawValue(_ rawValue: [String: Any]) -> Service? { - guard let serviceIdentifier = rawValue["statefulPluginIdentifier"] as? String, + guard let serviceIdentifier = rawValue["serviceIdentifier"] as? String, let rawState = rawValue["state"] as? Service.RawStateValue, let ServiceType = staticServicesByIdentifier[serviceIdentifier] else { diff --git a/Loop/Managers/ServicesManager.swift b/Loop/Managers/ServicesManager.swift index 7e62e95333..2393ceb073 100644 --- a/Loop/Managers/ServicesManager.swift +++ b/Loop/Managers/ServicesManager.swift @@ -100,7 +100,7 @@ class ServicesManager { } private func serviceTypeFromRawValue(_ rawValue: Service.RawStateValue) -> Service.Type? { - guard let identifier = rawValue["statefulPluginIdentifier"] as? String else { + guard let identifier = rawValue["serviceIdentifier"] as? String else { return nil } @@ -400,3 +400,15 @@ extension ServicesManager: ServiceOnboardingDelegate { extension ServicesManager { var availableSupports: [SupportUI] { activeServices.compactMap { $0 as? SupportUI } } } + +// Service extension for rawValue +extension Service { + typealias RawValue = [String: Any] + + var rawValue: RawValue { + return [ + "serviceIdentifier": pluginIdentifier, + "state": rawState + ] + } +} From 1922a16aec0274a8ed54dcb46b95c2e0538c13f0 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Sat, 23 Sep 2023 10:53:15 -0500 Subject: [PATCH 35/49] Allow calls to hasNewPumpEvents that do not replace all pending doses --- Loop/Managers/DeviceDataManager.swift | 10 ++++++++-- Loop/Managers/LoopDataManager.swift | 20 ------------------- .../Store Protocols/DoseStoreProtocol.swift | 4 ++-- 3 files changed, 10 insertions(+), 24 deletions(-) diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index c44359e94c..b6cd35d3a6 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -1198,11 +1198,17 @@ extension DeviceDataManager: PumpManagerDelegate { setLastError(error: error) } - func pumpManager(_ pumpManager: PumpManager, hasNewPumpEvents events: [NewPumpEvent], lastReconciliation: Date?, completion: @escaping (_ error: Error?) -> Void) { + func pumpManager( + _ pumpManager: PumpManager, + hasNewPumpEvents events: [NewPumpEvent], + lastReconciliation: Date?, + replacePendingEvents: Bool, + completion: @escaping (_ error: Error?) -> Void) + { dispatchPrecondition(condition: .onQueue(queue)) log.default("PumpManager:%{public}@ hasNewPumpEvents (lastReconciliation = %{public}@)", String(describing: type(of: pumpManager)), String(describing: lastReconciliation)) - loopManager.addPumpEvents(events, lastReconciliation: lastReconciliation) { (error) in + 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)) } diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index d50fddb73f..b56cddd35b 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -741,26 +741,6 @@ extension LoopDataManager { } } - - /// Adds and stores new pump events - /// - /// - Parameters: - /// - events: The pump events to add - /// - completion: A closure called once upon completion - /// - lastReconciliation: The date that pump events were most recently reconciled against recorded pump history. Pump events are assumed to be reflective of delivery up until this point in time. If reservoir values are recorded after this time, they may be used to supplement event based delivery. - /// - error: An error explaining why the events could not be saved. - func addPumpEvents(_ events: [NewPumpEvent], lastReconciliation: Date?, completion: @escaping (_ error: DoseStore.DoseStoreError?) -> Void) { - doseStore.addPumpEvents(events, lastReconciliation: lastReconciliation) { (error) in - completion(error) - - self.dataAccessQueue.async { - if error == nil { - self.clearCachedInsulinEffects() - } - } - } - } - /// Logs a new external bolus insulin dose in the DoseStore and HealthKit /// /// - Parameters: diff --git a/Loop/Managers/Store Protocols/DoseStoreProtocol.swift b/Loop/Managers/Store Protocols/DoseStoreProtocol.swift index ebbd104da4..dd21ea2a1f 100644 --- a/Loop/Managers/Store Protocols/DoseStoreProtocol.swift +++ b/Loop/Managers/Store Protocols/DoseStoreProtocol.swift @@ -35,8 +35,8 @@ protocol DoseStoreProtocol: AnyObject { var pumpEventQueryAfterDate: Date { get } // MARK: dose management - func addPumpEvents(_ events: [NewPumpEvent], lastReconciliation: Date?, completion: @escaping (_ error: DoseStore.DoseStoreError?) -> Void) - + func addPumpEvents(_ events: [NewPumpEvent], lastReconciliation: Date?, replacePendingEvents: Bool, completion: @escaping (_ error: DoseStore.DoseStoreError?) -> Void) + func addReservoirValue(_ unitVolume: Double, at date: Date, completion: @escaping (_ value: ReservoirValue?, _ previousValue: ReservoirValue?, _ areStoredValuesContinuous: Bool, _ error: DoseStore.DoseStoreError?) -> Void) func getNormalizedDoseEntries(start: Date, end: Date?, completion: @escaping (_ result: DoseStoreResult<[DoseEntry]>) -> Void) From 55cf35a91a06e271f3fd87ebaae8324f84cea126 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Sat, 23 Sep 2023 11:06:50 -0500 Subject: [PATCH 36/49] Update tests --- LoopTests/Mock Stores/MockDoseStore.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/LoopTests/Mock Stores/MockDoseStore.swift b/LoopTests/Mock Stores/MockDoseStore.swift index dc358ec361..207596f31b 100644 --- a/LoopTests/Mock Stores/MockDoseStore.swift +++ b/LoopTests/Mock Stores/MockDoseStore.swift @@ -11,7 +11,6 @@ import LoopKit @testable import Loop class MockDoseStore: DoseStoreProtocol { - var doseHistory: [DoseEntry]? var sensitivitySchedule: InsulinSensitivitySchedule? @@ -55,7 +54,7 @@ class MockDoseStore: DoseStoreProtocol { var lastAddedPumpData: Date - func addPumpEvents(_ events: [NewPumpEvent], lastReconciliation: Date?, completion: @escaping (DoseStore.DoseStoreError?) -> Void) { + func addPumpEvents(_ events: [NewPumpEvent], lastReconciliation: Date?, replacePendingEvents: Bool, completion: @escaping (DoseStore.DoseStoreError?) -> Void) { completion(nil) } From c75c8c8e8a868d64b2e453923da5e06516371027 Mon Sep 17 00:00:00 2001 From: Cameron Ingham Date: Fri, 29 Sep 2023 12:12:59 -0700 Subject: [PATCH 37/49] Update widgets for build with Xcode 15 --- .../Helpers/ContentMargin.swift | 20 +++++++++++++++++++ .../Helpers/WidgetBackground.swift | 8 +++++++- .../Widgets/SystemStatusWidget.swift | 1 + Loop.xcodeproj/project.pbxproj | 4 ++++ 4 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 Loop Widget Extension/Helpers/ContentMargin.swift diff --git a/Loop Widget Extension/Helpers/ContentMargin.swift b/Loop Widget Extension/Helpers/ContentMargin.swift new file mode 100644 index 0000000000..92a2d41786 --- /dev/null +++ b/Loop Widget Extension/Helpers/ContentMargin.swift @@ -0,0 +1,20 @@ +// +// ContentMargin.swift +// Loop Widget Extension +// +// Created by Cameron Ingham on 9/29/23. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import SwiftUI +import WidgetKit + +extension WidgetConfiguration { + func contentMarginsDisabledIfAvailable() -> some WidgetConfiguration { + if #available(iOSApplicationExtension 17.0, *) { + return self.contentMarginsDisabled() + } else { + return self + } + } +} diff --git a/Loop Widget Extension/Helpers/WidgetBackground.swift b/Loop Widget Extension/Helpers/WidgetBackground.swift index 9883f4917a..f5202f092c 100644 --- a/Loop Widget Extension/Helpers/WidgetBackground.swift +++ b/Loop Widget Extension/Helpers/WidgetBackground.swift @@ -11,6 +11,12 @@ import SwiftUI extension View { @ViewBuilder func widgetBackground() -> some View { - self.background { Color("WidgetBackground") } + if #available(iOSApplicationExtension 17.0, *) { + self.containerBackground(for: .widget) { + Color("WidgetBackground") + } + } else { + self.background { Color("WidgetBackground") } + } } } diff --git a/Loop Widget Extension/Widgets/SystemStatusWidget.swift b/Loop Widget Extension/Widgets/SystemStatusWidget.swift index 8546409b5c..a64096d2ad 100644 --- a/Loop Widget Extension/Widgets/SystemStatusWidget.swift +++ b/Loop Widget Extension/Widgets/SystemStatusWidget.swift @@ -76,5 +76,6 @@ struct SystemStatusWidget: Widget { .configurationDisplayName("Loop Status Widget") .description("See your current blood glucose and insulin delivery.") .supportedFamilies([.systemSmall, .systemMedium]) + .contentMarginsDisabledIfAvailable() } } diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index dcc947db00..deeb0885ce 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -253,6 +253,7 @@ 84AA81E32A4A36FB000B658B /* SystemActionLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AA81E22A4A36FB000B658B /* SystemActionLink.swift */; }; 84AA81E52A4A3981000B658B /* DeeplinkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AA81E42A4A3981000B658B /* DeeplinkManager.swift */; }; 84AA81E72A4A4DEF000B658B /* PumpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AA81E62A4A4DEF000B658B /* PumpView.swift */; }; + 84D2879F2AC756C8007ED283 /* ContentMargin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D2879E2AC756C8007ED283 /* ContentMargin.swift */; }; 891B508524342BE1005DA578 /* CarbAndBolusFlowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 891B508424342BE1005DA578 /* CarbAndBolusFlowViewModel.swift */; }; 892A5D59222F0A27008961AB /* Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 892A5D58222F0A27008961AB /* Debug.swift */; }; 892A5D692230C41D008961AB /* RangeReplaceableCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 892A5D682230C41D008961AB /* RangeReplaceableCollection.swift */; }; @@ -1182,6 +1183,7 @@ 84AA81E22A4A36FB000B658B /* SystemActionLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemActionLink.swift; sourceTree = ""; }; 84AA81E42A4A3981000B658B /* DeeplinkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeeplinkManager.swift; sourceTree = ""; }; 84AA81E62A4A4DEF000B658B /* PumpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpView.swift; sourceTree = ""; }; + 84D2879E2AC756C8007ED283 /* ContentMargin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentMargin.swift; sourceTree = ""; }; 891B508424342BE1005DA578 /* CarbAndBolusFlowViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbAndBolusFlowViewModel.swift; sourceTree = ""; }; 892A5D29222EF60A008961AB /* MockKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; name = MockKit.framework; path = Carthage/Build/iOS/MockKit.framework; sourceTree = SOURCE_ROOT; }; 892A5D2B222EF60A008961AB /* MockKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; name = MockKitUI.framework; path = Carthage/Build/iOS/MockKitUI.framework; sourceTree = SOURCE_ROOT; }; @@ -2532,6 +2534,7 @@ children = ( 84AA81DA2A4A2973000B658B /* Date.swift */, 84AA81D52A4A28AF000B658B /* WidgetBackground.swift */, + 84D2879E2AC756C8007ED283 /* ContentMargin.swift */, ); path = Helpers; sourceTree = ""; @@ -3640,6 +3643,7 @@ 14B1737328AEDBF6006CCD7C /* SystemStatusWidget.swift in Sources */, 84AA81DD2A4A2999000B658B /* StatusWidgetTimelineProvider.swift in Sources */, 14B1737528AEDBF6006CCD7C /* LoopCircleView.swift in Sources */, + 84D2879F2AC756C8007ED283 /* ContentMargin.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From b76ee03cb737b944de2ea2a6eb1b8aa36ccb3726 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Sun, 1 Oct 2023 10:01:51 -0500 Subject: [PATCH 38/49] Cleanup warnings --- Loop.xcodeproj/project.pbxproj | 3 +++ .../xcschemes/DoseMathTests.xcscheme | 2 +- .../xcschemes/Loop Intent Extension.xcscheme | 2 +- .../xcschemes/Loop Status Extension.xcscheme | 2 +- .../xcshareddata/xcschemes/Loop.xcscheme | 2 +- .../xcshareddata/xcschemes/LoopTests.xcscheme | 2 +- .../SmallStatusWidgetExtension.xcscheme | 2 +- .../xcshareddata/xcschemes/WatchApp.xcscheme | 18 +++++++++++------- Loop/Managers/AppExpirationAlerter.swift | 5 +++-- 9 files changed, 23 insertions(+), 15 deletions(-) diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index deeb0885ce..1181951609 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -3524,6 +3524,7 @@ }; C113F4472951352C00758735 /* Install Scenarios */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -3542,6 +3543,7 @@ }; C16DA84322E8E5FF008624C2 /* Install Plugins */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -3560,6 +3562,7 @@ }; C1D1405722FB66DF00DA6242 /* Build Derived Assets */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); diff --git a/Loop.xcodeproj/xcshareddata/xcschemes/DoseMathTests.xcscheme b/Loop.xcodeproj/xcshareddata/xcschemes/DoseMathTests.xcscheme index f225f4098a..a56f874c88 100644 --- a/Loop.xcodeproj/xcshareddata/xcschemes/DoseMathTests.xcscheme +++ b/Loop.xcodeproj/xcshareddata/xcschemes/DoseMathTests.xcscheme @@ -1,6 +1,6 @@ - + - + - + - + diff --git a/Loop/Managers/AppExpirationAlerter.swift b/Loop/Managers/AppExpirationAlerter.swift index e7768f92b6..eee1d90f4f 100644 --- a/Loop/Managers/AppExpirationAlerter.swift +++ b/Loop/Managers/AppExpirationAlerter.swift @@ -125,8 +125,8 @@ class AppExpirationAlerter { // If the target environment is a simulator, then // this is not a TestFlight distribution. Return false. #if targetEnvironment(simulator) - return false - #endif + return false + #else // If an "embedded.mobileprovision" is present in the main bundle, then // this is an Xcode, Ad-Hoc, or Enterprise distribution. Return false. @@ -143,6 +143,7 @@ class AppExpirationAlerter { // A TestFlight distribution presents a "sandboxReceipt", while an App Store // distribution presents a "receipt". Return true if we have a TestFlight receipt. return "sandboxReceipt".caseInsensitiveCompare(receiptName) == .orderedSame + #endif } static func calculateExpirationDate(profileExpiration: Date) -> Date { From 52ea3be839f971ffc55e464dcde9d23c13e952ee Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Sun, 1 Oct 2023 10:02:17 -0500 Subject: [PATCH 39/49] Include pending insulin in dosing decision --- Loop/Managers/LoopDataManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index b56cddd35b..2319f4eceb 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -1142,7 +1142,7 @@ extension LoopDataManager { dosingDecision.glucoseTargetRangeSchedule = settings.effectiveGlucoseTargetRangeSchedule() // These will be updated by updatePredictedGlucoseAndRecommendedDose, if possible - dosingDecision.predictedGlucose = predictedGlucose + dosingDecision.predictedGlucose = predictedGlucoseIncludingPendingInsulin dosingDecision.automaticDoseRecommendation = recommendedAutomaticDose?.recommendation // If the glucose prediction hasn't changed, then nothing has changed, so just use pre-existing recommendations From 33b001474b25d4ee4f20e0824163e5e49e549e16 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Sun, 22 Oct 2023 12:37:11 -0500 Subject: [PATCH 40/49] Updated translations from Lokalise on Sun Oct 22 11:55:50 CDT 2023 (#2089) --- .../it.lproj/Localizable.strings | 2 +- .../xcshareddata/xcschemes/WatchApp.xcscheme | 16 +- Loop/AppDelegate.swift | 2 + Loop/ar.lproj/Localizable.strings | 1 + Loop/cs.lproj/Localizable.strings | 1 + Loop/da.lproj/Localizable.strings | 4 +- Loop/de.lproj/Localizable.strings | 53 ++++- Loop/es.lproj/Localizable.strings | 4 +- Loop/fi.lproj/Localizable.strings | 4 +- Loop/fr.lproj/Localizable.strings | 4 +- Loop/he.lproj/Localizable.strings | 4 +- Loop/it.lproj/InfoPlist.strings | 3 + Loop/it.lproj/Localizable.strings | 181 +++++++++++++++++- Loop/ja.lproj/Localizable.strings | 4 +- Loop/nb.lproj/InfoPlist.strings | 3 + Loop/nb.lproj/Localizable.strings | 128 ++++++++++++- Loop/nl.lproj/Localizable.strings | 29 ++- Loop/pl.lproj/InfoPlist.strings | 3 + Loop/pl.lproj/Localizable.strings | 124 +++++++++++- Loop/pt-BR.lproj/Localizable.strings | 4 +- Loop/ro.lproj/Localizable.strings | 4 +- Loop/ru.lproj/Localizable.strings | 28 ++- Loop/sk.lproj/Localizable.strings | 1 + Loop/sv.lproj/Localizable.strings | 4 +- Loop/tr.lproj/Localizable.strings | 4 +- Loop/vi.lproj/Localizable.strings | 4 +- LoopUI/nb.lproj/Localizable.strings | 8 +- .../it.lproj/Localizable.strings | 4 +- .../pl.lproj/Localizable.strings | 2 +- WatchApp/pl.lproj/Interface.strings | 2 +- 30 files changed, 594 insertions(+), 41 deletions(-) diff --git a/Loop Status Extension/it.lproj/Localizable.strings b/Loop Status Extension/it.lproj/Localizable.strings index 871ef62d8c..9404086e35 100644 --- a/Loop Status Extension/it.lproj/Localizable.strings +++ b/Loop Status Extension/it.lproj/Localizable.strings @@ -14,7 +14,7 @@ "%1$@ v%2$@" = "%1$@ contro %2$@"; /* Widget label title describing the active carbs */ -"Active Carbs" = "Carb Attivi"; +"Active Carbs" = "Carboidrati Attivi"; /* Widget label title describing the active insulin */ "Active Insulin" = "Insulina attiva"; diff --git a/Loop.xcodeproj/xcshareddata/xcschemes/WatchApp.xcscheme b/Loop.xcodeproj/xcshareddata/xcschemes/WatchApp.xcscheme index 531646d9b4..6ab6be0246 100644 --- a/Loop.xcodeproj/xcshareddata/xcschemes/WatchApp.xcscheme +++ b/Loop.xcodeproj/xcshareddata/xcschemes/WatchApp.xcscheme @@ -88,10 +88,8 @@ debugServiceExtension = "internal" allowLocationSimulation = "YES" notificationPayloadFile = "WatchApp Extension/PushNotificationPayload.apns"> - + - + - + - + diff --git a/Loop/AppDelegate.swift b/Loop/AppDelegate.swift index 5da6ce9cb6..ebb05d5c12 100644 --- a/Loop/AppDelegate.swift +++ b/Loop/AppDelegate.swift @@ -22,6 +22,8 @@ final class AppDelegate: UIResponder, UIApplicationDelegate, WindowProvider { setenv("CFNETWORK_DIAGNOSTICS", "3", 1) + log.default("lastPathComponent = %{public}@", String(describing: Bundle.main.appStoreReceiptURL?.lastPathComponent)) + loopAppManager.initialize(windowProvider: self, launchOptions: launchOptions) loopAppManager.launch() return loopAppManager.isLaunchComplete diff --git a/Loop/ar.lproj/Localizable.strings b/Loop/ar.lproj/Localizable.strings index 5f46eb71e6..93db5473fa 100644 --- a/Loop/ar.lproj/Localizable.strings +++ b/Loop/ar.lproj/Localizable.strings @@ -306,6 +306,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "موافق"; diff --git a/Loop/cs.lproj/Localizable.strings b/Loop/cs.lproj/Localizable.strings index e4c3661250..effad5b9ea 100644 --- a/Loop/cs.lproj/Localizable.strings +++ b/Loop/cs.lproj/Localizable.strings @@ -67,6 +67,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "OK"; diff --git a/Loop/da.lproj/Localizable.strings b/Loop/da.lproj/Localizable.strings index 2bd3dbc3b9..45da938a4d 100644 --- a/Loop/da.lproj/Localizable.strings +++ b/Loop/da.lproj/Localizable.strings @@ -707,7 +707,8 @@ /* Details for missing data error when momentum effects are missing */ "Momentum effects" = "Momentumeffekter"; -/* Text for more info action on notification of upcoming profile expiration */ +/* Text for more info action on notification of upcoming profile expiration + Text for more info action on notification of upcoming TestFlight expiration */ "More Info" = "Mere Info"; /* Label for button to mute all alerts */ @@ -777,6 +778,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "OK"; diff --git a/Loop/de.lproj/Localizable.strings b/Loop/de.lproj/Localizable.strings index e775d574a1..dc4c9b5d4b 100755 --- a/Loop/de.lproj/Localizable.strings +++ b/Loop/de.lproj/Localizable.strings @@ -174,6 +174,15 @@ /* The title of the section containing algorithm settings */ "Algorithm Settings" = "Algorithmus-Einstellungen"; +/* Label for when mute alert will end */ +"All alerts muted until" = "Alle Alarme stummgeschaltet bis"; + +/* No comment provided by engineer. */ +"All Favorites" = "Alle Favoriten"; + +/* Label for carb quantity entry row on carb entry screen */ +"Amount Consumed" = "KH-Menge gegessen"; + /* The title of the Amplitude service */ "Amplitude" = "Amplitude"; @@ -216,6 +225,9 @@ /* Confirmation message for deleting a CGM */ "Are you sure you want to delete this CGM?" = "Bist Du sicher, dass Du dieses CGM löschen möchtest?"; +/* No comment provided by engineer. */ +"Are you sure you want to delete this food?" = "Bist Du sicher, dass Du diesen Favoriten löschen möchtest?"; + /* Confirmation message for deleting a service */ "Are you sure you want to delete this service?" = "Bist Du sicher, dass Du diesen Dienst löschen möchtest?"; @@ -303,6 +315,9 @@ /* Description of the prediction input effect for carbohydrates. (1: The glucose unit string) */ "Carbs Absorbed (g) ÷ Carb Ratio (g/U) × Insulin Sensitivity (%1$@/U)" = "Resorbierte Kohlenhydrate (g) ÷ Kohlenhydratfaktor (g/IE) × Insulinempfindlichkeit (%1$@/IE)"; +/* No comment provided by engineer. */ +"Caution" = "Achtung"; + /* The notification alert describing a low pump battery */ "Change the pump battery immediately" = "Wechsel sofort die Pumpenbatterie"; @@ -324,6 +339,9 @@ /* Carb entry section footer text explaining absorption time */ "Choose a longer absorption time for larger meals, or those containing fats and proteins. This is only guidance to the algorithm and need not be exact." = "Wähle eine längere Resorptionsdauer für größere Mahlzeiten oder welche die viel Fett und Proteine beinhalten. Dies ist eine Unterstützung für den Algorithmus und muss nicht genau sein."; +/* No comment provided by engineer. */ +"Choose Favorite:" = "Wähle Favorit:"; + /* Button title to close view The button label of the action used to dismiss the unsafe notification permission alert */ "Close" = "Schließen"; @@ -401,6 +419,9 @@ /* No comment provided by engineer. */ "Delete" = "Löschen"; +/* No comment provided by engineer. */ +"Delete “%@”?" = "„ %@ “ löschen?"; + /* The title of the button to remove the credentials for a service */ "Delete Account" = "Konto löschen"; @@ -410,6 +431,9 @@ /* Button title to delete CGM */ "Delete CGM" = "CGM löschen"; +/* No comment provided by engineer. */ +"Delete Food" = "Essen löschen"; + /* Button title to delete a service */ "Delete Service" = "Dienst löschen"; @@ -456,6 +480,12 @@ /* Message to the user to enable bluetooth */ "Enable\nBluetooth" = "Bluetooth einschalten"; +/* Title for Glucose Based Partial Application toggle */ +"Enable Glucose Based Partial Application" = "Glucose Based Partial Application aktivieren"; + +/* Title for Integral Retrospective Correction toggle */ +"Enable Integral Retrospective Correction" = "Integral Retrospective Correction aktivieren"; + /* The action hint of the workout mode toggle button when disabled */ "Enables" = "Aktivieren"; @@ -507,6 +537,9 @@ /* The alert title for a resume error */ "Failed to Resume Insulin Delivery" = "Wiederaufnahme der Insulinabgabe fehlgeschlagen"; +/* No comment provided by engineer. */ +"FAVORITE FOODS" = "Favorisiertes Essen"; + /* Title of insulin model preset */ "Fiasp" = "Fiasp"; @@ -559,6 +592,9 @@ /* Immediate Delivery status text */ "Immediate" = "Sofort"; +/* Algorithm Experiments description second paragraph. */ +"In future versions of Loop these experiments may change, end up as standard parts of the Loop Algorithm, or be removed from Loop entirely. Please follow along in the Loop Zulip chat to stay informed of possible changes to these features." = "In zukünftigen Versionen von Loop können sich diese Experimente ändern, als Standardbestandteile des Loop-Algorithmus enden oder vollständig aus Loop entfernt werden. Bitte folgen Sie dem Loop Zulip-Chat, um über mögliche Änderungen dieser Funktionen auf dem Laufenden zu bleiben."; + /* The title of a target alert action specifying an indefinitely long workout targets duration */ "Indefinitely" = "Unbegrenzt"; @@ -597,6 +633,13 @@ /* Insulin type label */ "Insulin Type" = "Insulintyp"; +/* Title for integral retrospective correction experiment description + Title of integral retrospective correction experiment */ +"Integral Retrospective Correction" = "Integrale retrospektive Korrektur"; + +/* Description of Integral Retrospective Correction toggle. */ +"Integral Retrospective Correction (IRC) is an extension of the standard Retrospective Correction (RC) algorithm component in Loop, which adjusts the forecast based on the history of discrepancies between predicted and actual glucose levels.\n\nIn contrast to RC, which looks at discrepancies over the last 30 minutes, with IRC, the history of discrepancies adds up over time. So continued positive discrepancies over time will result in increased dosing. If the discrepancies are negative over time, Loop will reduce dosing further." = "Integral Retrospective Correction (IRC) ist eine Erweiterung der standardmäßigen Retrospective Correction (RC)-Algorithmuskomponente in Loop, die die Prognose basierend auf der Historie der Abweichungen zwischen vorhergesagten und tatsächlichen Glukosewerten anpasst. \n\nIm Gegensatz zu RC, das die Abweichungen der letzten 30 Minuten betrachtet, summiert sich bei IRC der Verlauf der Abweichungen im Laufe der Zeit. Daher führen anhaltende positive Abweichungen im Laufe der Zeit zu einer erhöhten Dosierung. Wenn die Abweichungen im Laufe der Zeit negativ sind, reduziert Loop die Dosierung weiter."; + /* Description of an interrupted bolus dose entry (1: title for dose type, 2: value (? if no value) in bold, 3: programmed value (? if no value), 4: unit) */ "Interrupted %1$@: %2$@ of %3$@ %4$@" = "%1$@ unterbrochen: %2$@ von %3$@ %4$@"; @@ -658,6 +701,9 @@ /* The notification alert describing a long-lasting loop failure. The substitution parameter is the time interval since the last loop */ "Loop has not completed successfully in %@" = "Loop wurde nicht erfolgreich abgeschlossen seit %@"; +/* Description of Glucose Based Partial Application toggle. */ +"Loop normally gives 40% of your predicted insulin needs each dosing cycle.\n\nWhen the Glucose Based Partial Application experiment is enabled, Loop will vary the percentage of recommended bolus delivered each cycle with glucose level.\n\nNear correction range, it will use 20% (similar to Temp Basal), and gradually increase to a maximum of 80% at high glucose (200 mg/dL, 11.1 mmol/L).\n\nPlease be aware that during fast rising glucose, such as after an unannounced meal, this feature, combined with velocity and retrospective correction effects, may result in a larger dose than your ISF would call for." = "Loop liefert normalerweise in jedem Dosierungszyklus 40%1$ Ihres vorhergesagten Insulinbedarfs. \n\nWenn das Experiment Glucose Based Partial Application aktiviert ist, variiert Loop den Prozentsatz des empfohlenen Bolus, der in jedem Zyklus abgegeben wird, mit dem Glukosespiegel. \n\nIn der Nähe des Korrekturbereichs werden 20%2$ verwendet (ähnlich wie bei Temp Basal) und bei hohem Glukosewert (200 mg/dL, 11,1 mmol/L) allmählich auf ein Maximum von 80%3$ erhöht. \n\nBitte beachte, dass diese Funktion bei schnell ansteigendem Blutzucker, z. B. nach einer unangekündigten Mahlzeit, in Kombination mit Geschwindigkeits- und retrospektiven Korrektureffekten zu einer höheren Dosis führen kann, als Dein ISF erfordern würde."; + /* Description string for automatic bolus dosing strategy */ "Loop will automatically bolus when insulin needs are above scheduled basal, and will use temporary basal rates when needed to reduce insulin delivery below scheduled basal." = "Loop gibt automatisch einen Bolus ab, wenn der Insulinbedarf über der geplanten Basalrate liegt, und verwendet temporäre Basalraten, wenn dies erforderlich ist, um die Insulinabgabe unter die geplante Basalrate zu reduzieren."; @@ -707,7 +753,8 @@ /* Details for missing data error when momentum effects are missing */ "Momentum effects" = "Momentum-Effekte"; -/* Text for more info action on notification of upcoming profile expiration */ +/* Text for more info action on notification of upcoming profile expiration + Text for more info action on notification of upcoming TestFlight expiration */ "More Info" = "Weitere Info"; /* Label for button to mute all alerts */ @@ -777,6 +824,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "OK"; @@ -960,6 +1008,9 @@ /* Message presented in the status row instructing the user to tap this row to stop a bolus */ "Tap to Stop" = "Stoppen"; +/* The alert body for unmute alert confirmation */ +"Tap Unmute to resume sound for your alerts and alarms." = "Tippe auf Stummschaltung aufheben, um den Ton für Deine Warnungen und Alarme wieder aufzunehmen."; + /* Alert message for a bolus too small validation error */ "The bolus amount entered is smaller than the minimum deliverable." = "Die eingegebene Bolusmenge ist kleiner als die Mindestabgabemenge."; diff --git a/Loop/es.lproj/Localizable.strings b/Loop/es.lproj/Localizable.strings index 728856649f..a1fd7ee4d9 100644 --- a/Loop/es.lproj/Localizable.strings +++ b/Loop/es.lproj/Localizable.strings @@ -692,7 +692,8 @@ /* Details for missing data error when momentum effects are missing */ "Momentum effects" = "Efectos de Momento"; -/* Text for more info action on notification of upcoming profile expiration */ +/* Text for more info action on notification of upcoming profile expiration + Text for more info action on notification of upcoming TestFlight expiration */ "More Info" = "Más Info"; /* Label for button to mute all alerts */ @@ -759,6 +760,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "OK"; diff --git a/Loop/fi.lproj/Localizable.strings b/Loop/fi.lproj/Localizable.strings index c05de83937..9ca9c0a007 100644 --- a/Loop/fi.lproj/Localizable.strings +++ b/Loop/fi.lproj/Localizable.strings @@ -543,7 +543,8 @@ /* Details for missing data error when momentum effects are missing */ "Momentum effects" = "Liikevaikutukset (momentum)"; -/* Text for more info action on notification of upcoming profile expiration */ +/* Text for more info action on notification of upcoming profile expiration + Text for more info action on notification of upcoming TestFlight expiration */ "More Info" = "Lisätietoa"; /* Sensor state description for the non-valid state */ @@ -583,6 +584,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "OK"; diff --git a/Loop/fr.lproj/Localizable.strings b/Loop/fr.lproj/Localizable.strings index d1631febe7..95225a94ed 100644 --- a/Loop/fr.lproj/Localizable.strings +++ b/Loop/fr.lproj/Localizable.strings @@ -683,7 +683,8 @@ /* Details for missing data error when momentum effects are missing */ "Momentum effects" = "Effets de momentum"; -/* Text for more info action on notification of upcoming profile expiration */ +/* Text for more info action on notification of upcoming profile expiration + Text for more info action on notification of upcoming TestFlight expiration */ "More Info" = "Plus d'informations"; /* Label for button to mute all alerts */ @@ -750,6 +751,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "OK"; diff --git a/Loop/he.lproj/Localizable.strings b/Loop/he.lproj/Localizable.strings index 6f65b8dea7..2ca902a1e0 100644 --- a/Loop/he.lproj/Localizable.strings +++ b/Loop/he.lproj/Localizable.strings @@ -476,7 +476,8 @@ /* Details for missing data error when momentum effects are missing */ "Momentum effects" = "השפעות מומנטום"; -/* Text for more info action on notification of upcoming profile expiration */ +/* Text for more info action on notification of upcoming profile expiration + Text for more info action on notification of upcoming TestFlight expiration */ "More Info" = "מידע נוסף"; /* Label for button to mute all alerts */ @@ -546,6 +547,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "אישור"; diff --git a/Loop/it.lproj/InfoPlist.strings b/Loop/it.lproj/InfoPlist.strings index 9e861d4c3a..f86acc5698 100644 --- a/Loop/it.lproj/InfoPlist.strings +++ b/Loop/it.lproj/InfoPlist.strings @@ -4,6 +4,9 @@ /* Bundle name */ "CFBundleName" = "$(PRODUCT_NAME)"; +/* Privacy - NFC Scan Usage Description */ +"NFCReaderUsageDescription" = "L'app utilizza NFC per l'accoppiamento con i dispositivi per il diabete."; + /* Privacy - Bluetooth Always Usage Description */ "NSBluetoothAlwaysUsageDescription" = "Il Bluetooth è utilizzato per comunicare con il microinfusore ed il sensore glicemico"; diff --git a/Loop/it.lproj/Localizable.strings b/Loop/it.lproj/Localizable.strings index 6e31a65780..0528bed0d4 100644 --- a/Loop/it.lproj/Localizable.strings +++ b/Loop/it.lproj/Localizable.strings @@ -4,6 +4,9 @@ /* Status row title for premeal override enabled (leading space is to separate from symbol) */ " Pre-meal Preset" = "Preimpostazioni del Pre-Pasto"; +/* remaining time in setting's profile expiration section */ +" remaining" = "Rimanente"; + /* Warning text for when Notifications or Critical Alerts Permissions is disabled */ " Safety Notifications are OFF" = "Le notifiche di sicurezza risultano spente"; @@ -92,11 +95,14 @@ Description of a bolus dose entry (1: title for dose type, 2: value (? if no value) in bold, 3: unit) */ "%1$@: %2$@ %3$@" = "%1$@ : %2$@ %3$@"; +/* No comment provided by engineer. */ +"⚠️" = "⚠️"; + /* Description of the prediction input effect for glucose momentum */ "15 min glucose regression coefficient (b₁), continued with decay over 30 min" = "Coefficiente di regressione del glucosio a 15 min (b₁), interpolato con il decadimento a 30 min."; /* Description of the prediction input effect for retrospective correction */ -"30 min comparison of glucose prediction vs actual, continued with decay over 60 min" = "30 min di confronto tra la previsione glicemica e quella attuale, proseguita con il degrado sino a 60 minuti"; +"30 min comparison of glucose prediction vs actual, continued with decay over 60 min" = "Confronto di 30 minuti tra la previsione del glucosio e quella effettiva, continuato con decadimento per 60 minuti"; /* Estimated remaining duration with a few seconds */ "A few seconds remaining" = "Pochi secondi rimanenti"; @@ -132,7 +138,7 @@ "Active Carbohydrates: %@" = "Carboidrati attivi: %@"; /* Title describing quantity of still-absorbing carbohydrates */ -"Active Carbs" = "Carb Attivi"; +"Active Carbs" = "Carboidrati Attivi"; /* The title of the Insulin On-Board graph */ "Active Insulin" = "Insulina attiva"; @@ -140,6 +146,9 @@ /* The string format describing active insulin. (1: localized insulin value description) */ "Active Insulin: %@" = "Insulina attiva: %@"; +/* No comment provided by engineer. */ +"Add a new favorite food" = "Aggiungi un nuovo cibo preferito"; + /* Title of the user activity for adding carbs */ "Add Carb Entry" = "Agg. Carb. Assunti"; @@ -168,9 +177,28 @@ Notification & Critical Alert Permissions screen title */ "Alert Permissions" = "Avvisi"; +/* Navigation title for algorithms experiments screen + The title of the Algorithm Experiments section in settings */ +"Algorithm Experiments" = "Esperimenti sugli algoritmi"; + +/* Algorithm Experiments description. */ +"Algorithm Experiments are optional modifications to the Loop Algorithm. These modifications are less tested than the standard Loop Algorithm, so please use carefully." = "Gli esperimenti sull'algoritmo sono modifiche opzionali all'algoritmo del loop. Queste modifiche sono meno testate rispetto all'algoritmo Loop standard, quindi utilizzale con Attenzione."; + /* The title of the section containing algorithm settings */ "Algorithm Settings" = "Impostazioni Algoritmo"; +/* Warning text for when alerts are muted */ +"All Alerts Muted" = "Tutti gli avvisi silenziati"; + +/* Label for when mute alert will end */ +"All alerts muted until" = "silenzia tutti gli avvisi fino"; + +/* No comment provided by engineer. */ +"All Favorites" = "Tutti i preferiti"; + +/* Label for carb quantity entry row on carb entry screen */ +"Amount Consumed" = "Quantità consumata"; + /* The title of the Amplitude service */ "Amplitude" = "Amplitude"; @@ -195,6 +223,9 @@ /* The title of the nightscout API secret credential */ "API Secret" = "Chiave personale API"; +/* Settings app profile section */ +"App Profile" = "Profilo App"; + /* Action sheet confirmation message for pump history deletion */ "Are you sure you want to delete all history entries?" = "Sei sicuro di voler eliminare tutte le voci della cronologia?"; @@ -210,6 +241,9 @@ /* Confirmation message for deleting a CGM */ "Are you sure you want to delete this CGM?" = "Sei sicuro di voler eliminare questo CGM?"; +/* No comment provided by engineer. */ +"Are you sure you want to delete this food?" = "Sei sicuro di voler cancellare questo cibo?"; + /* Confirmation message for deleting a service */ "Are you sure you want to delete this service?" = "Sei sicuro di voler eliminare questo servizio?"; @@ -297,6 +331,9 @@ /* Description of the prediction input effect for carbohydrates. (1: The glucose unit string) */ "Carbs Absorbed (g) ÷ Carb Ratio (g/U) × Insulin Sensitivity (%1$@/U)" = "Carboidrati Assorbiti ÷ Rapporto Carboidrati (gr/U) × Sensibilità Insulinica (%1$@/U)"; +/* No comment provided by engineer. */ +"Caution" = "Attenzione"; + /* The notification alert describing a low pump battery */ "Change the pump battery immediately" = "Cambiare immediatamente la batteria del microinfusore"; @@ -307,7 +344,7 @@ "Check settings" = "Controllare le impostazioni"; /* Recovery suggestion when reservoir data is missing */ -"Check that your pump is in range" = "Controlllare che il microinfusore si trovi vicino"; +"Check that your pump is in range" = "Controllare che il microinfusore si trovi vicino"; /* Recovery suggestion when glucose data is missing */ "Check your CGM data source" = "Controllare la sorgente dati del sensore"; @@ -318,6 +355,9 @@ /* Carb entry section footer text explaining absorption time */ "Choose a longer absorption time for larger meals, or those containing fats and proteins. This is only guidance to the algorithm and need not be exact." = "Scegli un tempo di assorbimento piu lungo per i pasti piu grandi o quelli contenenti grassi e proteine. Questa e solo una guida all’algoritmo e non e necessario che sia esatta."; +/* No comment provided by engineer. */ +"Choose Favorite:" = "Scegli il preferito:"; + /* Button title to close view The button label of the action used to dismiss the unsafe notification permission alert */ "Close" = "Chiudi"; @@ -362,6 +402,9 @@ The title text for the glucose target range schedule */ "Correction Range" = "Intervallo Glicemico"; +/* Format string for title of reset loop alert. (1: App name) */ +"Could Not Restart %1$@" = "Impossibile riavviare %1$@"; + /* Critical Alerts Status text */ "Critical Alerts" = "Avvisi critici"; @@ -395,6 +438,9 @@ /* No comment provided by engineer. */ "Delete" = "Cancella"; +/* No comment provided by engineer. */ +"Delete “%@”?" = "Cancellare \"%@\" ?"; + /* The title of the button to remove the credentials for a service */ "Delete Account" = "Cancella Account"; @@ -404,6 +450,9 @@ /* Button title to delete CGM */ "Delete CGM" = "Elimina CGM"; +/* No comment provided by engineer. */ +"Delete Food" = "Cancella cibo"; + /* Button title to delete a service */ "Delete Service" = "Elimina Servizio"; @@ -444,9 +493,18 @@ /* The title of the Dosing Strategy section in settings */ "Dosing Strategy" = "Strategia di dosaggio"; +/* Override error description: duration exceed max (1: max duration in hours). */ +"Duration exceeds: %1$.1f hours" = "La durata supera: %1$.1f ore"; + /* Message to the user to enable bluetooth */ "Enable\nBluetooth" = "Abilita\n Bluetooth"; +/* Title for Glucose Based Partial Application toggle */ +"Enable Glucose Based Partial Application" = "Abilita l'applicazione parziale basata sul glucosio"; + +/* Title for Integral Retrospective Correction toggle */ +"Enable Integral Retrospective Correction" = "Abilita Correzione Retrospettiva Integrale"; + /* The action hint of the workout mode toggle button when disabled */ "Enables" = "Abilita"; @@ -498,12 +556,18 @@ /* The alert title for a resume error */ "Failed to Resume Insulin Delivery" = "Impossibile riprendere l'erogazione dell'insulina"; +/* No comment provided by engineer. */ +"FAVORITE FOODS" = "CIBI SALVATI"; + /* Title of insulin model preset */ "Fiasp" = "Fiasp"; /* Label for manual glucose entry row on bolus screen */ "Fingerstick Glucose" = "Glicemia da dito"; +/* Secondary text for alerts disabled warning, which appears on the main status screen. */ +"Fix now by turning Notifications, Critical Alerts and Time Sensitive Notifications ON." = "Risolvilo ora attivando Notifiche, Avvisi critici e Notifiche urgenti."; + /* The format string used to describe a finite workout targets duration */ "For %1$@" = "Per %1$@"; @@ -522,6 +586,10 @@ /* The title of the glucose and prediction graph */ "Glucose" = "Glicemia"; +/* Title for glucose based partial application experiment description + Title of glucose based partial application experiment */ +"Glucose Based Partial Application" = "Glucose Based Partial Application (GBPA)"; + /* The error message when glucose data is too old to be used. (1: glucose data age in minutes) */ "Glucose data is %1$@ old" = "I dati sulla glicemia sono %1$@ vecchi"; @@ -531,6 +599,9 @@ /* Alert title when glucose data returns while on bolus screen */ "Glucose Data Now Available" = "Dati Glicemie ora disponibili"; +/* Description of the prediction input effect for suspension of insulin delivery */ +"Glucose effect of suspending insulin delivery" = "Effetto sulla glicemia della sospensione della somministrazione di insulina"; + /* Alert title for a manual glucose entry out of range error Title for bolus screen warning when glucose entry is out of range */ "Glucose Entry Out of Range" = "Glicemia inserita fuori dall'intervallo"; @@ -541,9 +612,15 @@ /* Details for configuration error when glucose target range schedule is missing */ "Glucose Target Range Schedule" = "Programma degli intervalli degli obiettivi glicemici"; +/* The title text for how to update */ +"How to update (LoopDocs)" = "Come Aggiornare (LoopDocs)"; + /* Immediate Delivery status text */ "Immediate" = "Immediato"; +/* Algorithm Experiments description second paragraph. */ +"In future versions of Loop these experiments may change, end up as standard parts of the Loop Algorithm, or be removed from Loop entirely. Please follow along in the Loop Zulip chat to stay informed of possible changes to these features." = "Nelle versioni future di Loop questi esperimenti potrebbero cambiare, diventare parti standard dell'algoritmo Loop o essere rimossi completamente da Loop. Segui la chat di Loop Zulip per rimanere informato su possibili modifiche a queste funzionalità."; + /* The title of a target alert action specifying an indefinitely long workout targets duration */ "Indefinitely" = "A tempo indeterminato"; @@ -582,9 +659,22 @@ /* Insulin type label */ "Insulin Type" = "Tipo d'insulina"; +/* Title for integral retrospective correction experiment description + Title of integral retrospective correction experiment */ +"Integral Retrospective Correction" = "Correzione retrospettiva Integrale"; + +/* Description of Integral Retrospective Correction toggle. */ +"Integral Retrospective Correction (IRC) is an extension of the standard Retrospective Correction (RC) algorithm component in Loop, which adjusts the forecast based on the history of discrepancies between predicted and actual glucose levels.\n\nIn contrast to RC, which looks at discrepancies over the last 30 minutes, with IRC, the history of discrepancies adds up over time. So continued positive discrepancies over time will result in increased dosing. If the discrepancies are negative over time, Loop will reduce dosing further." = "La Correzione Retrospettiva Integrale (IRC) è un'estensione del componente standard dell'algoritmo di correzione retrospettiva (RC) in Loop, che regola la previsione in base alla cronologia delle discrepanze tra i livelli di glucosio previsti e quelli effettivi. \n\nA differenza di RC, che esamina le discrepanze negli ultimi 30 minuti, con IRC la cronologia delle discrepanze si accumula nel tempo. Pertanto, continue discrepanze positive nel tempo comporteranno un aumento del dosaggio. Se le discrepanze diventano negative nel tempo, Loop ridurrà ulteriormente il dosaggio."; + /* Description of an interrupted bolus dose entry (1: title for dose type, 2: value (? if no value) in bold, 3: programmed value (? if no value), 4: unit) */ "Interrupted %1$@: %2$@ of %3$@ %4$@" = "Interrotto %1$@ : %2$@ di %3$@ %4$@"; +/* Carb error description: invalid absorption time. (1: Input duration in hours). */ +"Invalid absorption time: %1$@ hours" = "Tempo di assorbimento non valido: %d ore"; + +/* Bolus error description: invalid bolus amount. */ +"Invalid Bolus Amount" = "Quantità di bolo non valida"; + /* Carb error description: invalid carb amount. */ "Invalid carb amount" = "Quantità di carboidrati non valida"; @@ -600,6 +690,9 @@ /* The title text for the issue report cell */ "Issue Report" = "Report dei problemi"; +/* The notification description for a meal that was possibly not logged in Loop. */ +"It looks like you may not have logged a meal you ate. Tap to log it now." = "Sembra che non sia stato registrato un pasto consumato. Toccare per registrarlo ora."; + /* Title of the warning shown when a large meal was entered */ "Large Meal Entered" = "Pasto abbondante inserito"; @@ -631,9 +724,15 @@ /* Bluetooth unavailable alert body. */ "Loop has detected an issue with your Bluetooth settings, and will not work successfully until Bluetooth is enabled. You will not receive glucose readings, or be able to bolus." = "Loop ha rilevato un problema con le tue impostazioni Bluetooth e non funzionerà correttamente finché il Bluetooth non sarà abilitato. Non riceverai letture glicemiche né potrai eseguire il bolo."; +/* Warning displayed when user is adding a meal from an missed meal notification */ +"Loop has detected an missed meal and estimated its size. Edit the carb amount to match the amount of any carbs you may have eaten." = "Loop ha rilevato un pasto saltato e ne ha stimato le dimensioni. Modifica la quantità di carboidrati in modo che corrisponda alla quantità di carboidrati che potresti aver mangiato."; + /* The notification alert describing a long-lasting loop failure. The substitution parameter is the time interval since the last loop */ "Loop has not completed successfully in %@" = "Loop non ha funzionato correttamente per %@"; +/* Description of Glucose Based Partial Application toggle. */ +"Loop normally gives 40% of your predicted insulin needs each dosing cycle.\n\nWhen the Glucose Based Partial Application experiment is enabled, Loop will vary the percentage of recommended bolus delivered each cycle with glucose level.\n\nNear correction range, it will use 20% (similar to Temp Basal), and gradually increase to a maximum of 80% at high glucose (200 mg/dL, 11.1 mmol/L).\n\nPlease be aware that during fast rising glucose, such as after an unannounced meal, this feature, combined with velocity and retrospective correction effects, may result in a larger dose than your ISF would call for." = "Loop normalmente fornisce il 40%1$ del fabbisogno di insulina previsto per ogni ciclo di dosaggio. \n\n Quando l'esperimento di Applicazione Parziale Basata sul Glucosio (GBPA) è abilitato, il Loop varierà la percentuale del bolo consigliato erogato ad ogni ciclo con il livello di glucosio. \n\nVicino all'intervallo di correzione, utilizzerà il 20%2$ (simile alla basale temporanea) e aumenterà gradualmente fino a un massimo dell'80%3$ in caso di glicemia elevata (200 mg/dl, 11,1 mmol/l). \n\nTieni presente che durante un rapido aumento della glicemia, ad esempio dopo un pasto imprevisto, questa caratteristica, combinata con la velocità e gli effetti di correzione retrospettiva, può comportare una dose maggiore di quella richiesta dall'FSI."; + /* Description string for automatic bolus dosing strategy */ "Loop will automatically bolus when insulin needs are above scheduled basal, and will use temporary basal rates when needed to reduce insulin delivery below scheduled basal." = "Loop eseguirà automaticamente il bolo quando il fabbisogno d'insulina è superiore alla basale programmata e utilizzerà velocità basali temporanee quando necessario per ridurre l'erogazione d'insulina al di sotto della basale programmata."; @@ -668,6 +767,9 @@ /* The short unit display string for milligrams of glucose per decilter */ "mg/dL" = "mg/dL"; +/* Title for missed meal notifications toggle */ +"Missed Meal Notifications" = "Notifiche di pasti mancati"; + /* The error message for missing data. (1: missing data details) */ "Missing data: %1$@" = "Dati mancanti: %1$@"; @@ -680,18 +782,28 @@ /* Details for missing data error when momentum effects are missing */ "Momentum effects" = "Effetto glicemico attuale"; -/* Text for more info action on notification of upcoming profile expiration */ +/* Text for more info action on notification of upcoming profile expiration + Text for more info action on notification of upcoming TestFlight expiration */ "More Info" = "Piu info"; /* Label for button to mute all alerts */ "Mute All Alerts" = "Disattiva tutti gli avvisi"; +/* Title for mute alert duration selection action sheet */ +"Mute All Alerts Temporarily" = "silenzia tutti gli avvisi temporaneamente"; + /* Sensor state description for the non-valid state */ "Needs Attention" = "Esige Attenzione"; +/* Override error description: negative duration error. */ +"Negative duration not allowed" = "Durata negativa non consentita"; + /* The title of the Nightscout service */ "Nightscout" = "Nightscout"; +/* Message for mute alert duration selection action sheet */ +"No alerts or alarms will sound while muted. Select how long you would you like to mute for." = "Nessun avviso o allarme suonerà quando l'audio è disattivato. Seleziona per quanto tempo desideri disattivare l'audio."; + /* Title for bolus screen notice when no bolus is recommended Title for bolus screen warning when glucose is below suspend threshold, and a bolus is not recommended Title for bolus screen warning when no bolus is recommended */ @@ -747,6 +859,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "OK"; @@ -756,6 +869,9 @@ /* The title text for the override presets */ "Override Presets" = "Programma Alternativo"; +/* The notification title for a meal that was possibly not logged in Loop. */ +"Possible Missed Meal" = "Possibile pasto saltato"; + /* The label of the pre-meal mode toggle button */ "Pre-Meal Targets" = "Obiettivo Pre-Pasto"; @@ -774,9 +890,18 @@ /* Format string describing retrospective glucose prediction comparison. (1: Predicted glucose)(2: Actual glucose)(3: difference) */ "Predicted: %1$@\nActual: %2$@ (%3$@)" = "Previsto: %1$@\nEffettivo: %2$@ (%3$@)"; +/* Format string describing integral retrospective correction. (1: Integral glucose effect)(2: Total glucose effect) */ +"prediction-description-integral-retrospective-correction" = "previsione-descrizione-integrale-retrospettiva-correzione"; + /* Preparing critical event log text */ "Preparing Critical Event Logs" = "Lista degli eventi critici in preparazione"; +/* Settings App Profile expiration view */ +"Profile Expiration" = "Scadenza Profilo"; + +/* Time that profile expires */ +"Profile expires " = "Profilo scaduto"; + /* The title for notification of upcoming profile expiration */ "Profile Expires Soon" = "Il profilo scadra' presto"; @@ -851,15 +976,24 @@ /* The title of the notification action to retry a bolus command */ "Retry" = "Riprova"; +/* No comment provided by engineer. */ +"Save" = "Salva"; + /* Button text to save carbs and/or manual glucose entry and deliver a bolus */ "Save and Deliver" = "Salva e Invia"; +/* No comment provided by engineer. */ +"Save as favorite food" = "Salva come cibo preferito"; + /* Button text to save carbs and/or manual glucose entry without a bolus */ "Save without Bolusing" = "Salva senza bolo"; /* Scheduled Delivery status text */ "Scheduled" = "Programmato"; +/* No comment provided by engineer. */ +"Selecting a favorite food in the carb entry screen automatically fills in the carb quantity, food type, and absorption time fields! Tap the add button below to create your first favorite food!" = "Selezionando un alimento preferito nella schermata di immissione dei carboidrati si riempiono automaticamente i campi relativi alla quantità di carboidrati, al tipo di alimento e al tempo di assorbimento! Tocca il pulsante Aggiungi qui sotto per creare il tuo primo cibo preferito!"; + /* The title of the services section in settings */ "Services" = "Servizi"; @@ -887,6 +1021,9 @@ /* Software update button link text */ "Software Update" = "Aggiornamento software"; +/* Carb error description: invalid start time is out of range. */ +"Start time is out of range: %@" = "L'ora di inizio non rientra nell'intervallo: %@"; + /* The format for the description of a temporary override start date */ "starting at %@" = "inizia a %@"; @@ -900,6 +1037,9 @@ /* The title text in settings */ "Suspend Threshold" = "Sospendi Soglia"; +/* Title of the prediction input effect for suspension of insulin delivery */ +"Suspension of Insulin Delivery" = "Sospensione della somministrazione di insulina"; + /* Descriptive text for button to add CGM device */ "Tap here to set up a CGM" = "Premi per impostare un CGM"; @@ -918,6 +1058,12 @@ /* Message presented in the status row instructing the user to tap this row to stop a bolus */ "Tap to Stop" = "Interrompi"; +/* Label for button to unmute all alerts */ +"Tap to Unmute Alerts" = "Clicca per riattivare gli avvisi"; + +/* The alert body for unmute alert confirmation */ +"Tap Unmute to resume sound for your alerts and alarms." = "Tocca Riattiva per ripristinare l'audio per gli avvisi e le sveglie."; + /* Alert message for a bolus too small validation error */ "The bolus amount entered is smaller than the minimum deliverable." = "La quantità di bolo immessa è inferiore alla quantità minima erogabile."; @@ -948,6 +1094,9 @@ /* Title text for button to Therapy Settings */ "Therapy Settings" = "Impostazioni Terapia"; +/* String shown when glucose based partial application cannot be enabled because dosing strategy is not set to Automatic Bolus */ +"This option only applies when Loop's Dosing Strategy is set to Automatic Bolus." = "Questa opzione si applica solo quando la strategia di dosaggio di Loop è impostata su Bolo automatico."; + /* Time Sensitive Status text */ "Time Sensitive Notifications" = "Notifiche a tempo"; @@ -984,9 +1133,27 @@ /* The error message displayed for unknown errors. (1: unknown error) */ "Unknown Error: %1$@" = "Errore sconosciuto: %1$@"; +/* Override error description: unknown preset (1: preset name). */ +"Unknown preset: %1$@" = "Preimpostazione sconosciuta: %1$@"; + +/* Unknown amount of time in settings' profile expiration section */ +"Unknown time" = "Ora sconosciuta"; + +/* The title of the action used to unmute alerts */ +"Unmute" = "Riattiva"; + +/* The alert title for unmute alert confirmation */ +"Unmute Alerts?" = "Attivare gli avvisi?"; + +/* Error message when a service can't be found to handle a push notification. (1: Service Identifier) */ +"Unsupported Notification Service: %1$@" = "Servizio di notifica non supportato: %1$@"; + /* The format for the description of a temporary override end date */ "until %@" = "fino a %@"; +/* indication of when alerts will be unmuted (1: time when alerts unmute) */ +"Until %1$@" = "Fino al %1$@"; + /* The title of a target alert action specifying pre-meal targets duration for 1 hour or until the user enters carbs (whichever comes first). */ "Until I enter carbs" = "Fino a quando non inserisco carboidrati"; @@ -1014,9 +1181,15 @@ /* Explanation of suspend threshold */ "When current or forecasted glucose is below the suspend threshold, Loop will not recommend a bolus, and will always recommend a temporary basal rate of 0 units per hour." = "Quando la glicemia attuale o prevista è sotto la soglia di sospensione, Loop non consiglia un bolo, e raccomanda una velocità basale temporanea di 0 unità per ora."; +/* Description of missed meal notifications. */ +"When enabled, Loop can notify you when it detects a meal that wasn't logged." = "Se abilitato, Loop può avvisarti quando rileva un pasto che non è stato registrato."; + /* No comment provided by engineer. */ "When out of Closed Loop mode, the app uses a simplified bolus calculator like a typical pump." = "Quando non è in modalità ciclo chiuso, l'applicazione utilizza un calcolatore di bolo semplificato come un tipico microinfusore."; +/* Format string for message of reset loop alert. (1: App name) (2: error description) */ +"While trying to restart %1$@ an error occured.\n\n%2$@" = "Durante il tentativo di riavviare %1$@ si è verificato un errore.\n\n%2$@"; + /* The label of the workout mode toggle button */ "Workout Targets" = "Obiettivi di allenamento"; diff --git a/Loop/ja.lproj/Localizable.strings b/Loop/ja.lproj/Localizable.strings index 0971d5d384..0e443836e9 100644 --- a/Loop/ja.lproj/Localizable.strings +++ b/Loop/ja.lproj/Localizable.strings @@ -331,7 +331,8 @@ /* Details for missing data error when momentum effects are missing */ "Momentum effects" = "モメンタム効果"; -/* Text for more info action on notification of upcoming profile expiration */ +/* Text for more info action on notification of upcoming profile expiration + Text for more info action on notification of upcoming TestFlight expiration */ "More Info" = "詳細"; /* Sensor state description for the non-valid state */ @@ -348,6 +349,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "OK"; diff --git a/Loop/nb.lproj/InfoPlist.strings b/Loop/nb.lproj/InfoPlist.strings index 4e6970dff3..13ca15ffc2 100644 --- a/Loop/nb.lproj/InfoPlist.strings +++ b/Loop/nb.lproj/InfoPlist.strings @@ -4,6 +4,9 @@ /* Bundle name */ "CFBundleName" = "$(PRODUCT_NAME)"; +/* Privacy - NFC Scan Usage Description */ +"NFCReaderUsageDescription" = "Appen bruker NFC til å koble seg sammen med diabetesenheter."; + /* Privacy - Bluetooth Always Usage Description */ "NSBluetoothAlwaysUsageDescription" = "Bluetooth brukes til å kommunisere med insulinpumpe og kontinuerlige glukosemonitorer."; diff --git a/Loop/nb.lproj/Localizable.strings b/Loop/nb.lproj/Localizable.strings index 3a7e5ce6b0..4284ceb32d 100644 --- a/Loop/nb.lproj/Localizable.strings +++ b/Loop/nb.lproj/Localizable.strings @@ -2,7 +2,7 @@ " (pending: %@)" = "(venter: %@ )"; /* Status row title for premeal override enabled (leading space is to separate from symbol) */ -" Pre-meal Preset" = " Forhåndsinnstilling før måltid"; +" Pre-meal Preset" = "Forhåndsinnstilling før måltid"; /* remaining time in setting's profile expiration section */ " remaining" = "gjenstående"; @@ -95,6 +95,9 @@ Description of a bolus dose entry (1: title for dose type, 2: value (? if no value) in bold, 3: unit) */ "%1$@: %2$@ %3$@" = "%1$@ : %2$@ %3$@"; +/* No comment provided by engineer. */ +"⚠️" = "⚠️"; + /* Description of the prediction input effect for glucose momentum */ "15 min glucose regression coefficient (b₁), continued with decay over 30 min" = "15 minutters glukose-regresjonskoeffisient (b1), fortsatt med nedbrytning over 30 minutter."; @@ -143,6 +146,9 @@ /* The string format describing active insulin. (1: localized insulin value description) */ "Active Insulin: %@" = "Aktivt insulin: %@"; +/* No comment provided by engineer. */ +"Add a new favorite food" = "Legg til en ny favorittmat"; + /* Title of the user activity for adding carbs */ "Add Carb Entry" = "Legg til karbohydrater"; @@ -171,9 +177,28 @@ Notification & Critical Alert Permissions screen title */ "Alert Permissions" = "Varslingsinnstillinger"; +/* Navigation title for algorithms experiments screen + The title of the Algorithm Experiments section in settings */ +"Algorithm Experiments" = "Algoritmeeksperimenter"; + +/* Algorithm Experiments description. */ +"Algorithm Experiments are optional modifications to the Loop Algorithm. These modifications are less tested than the standard Loop Algorithm, so please use carefully." = "Algoritmeeksperimenter er valgfrie modifikasjoner til Loop-algoritmen. Disse modifikasjonene er mindre testet enn standard Loop-algoritmen, så vær vennlig å bruke dem med forsiktighet."; + /* The title of the section containing algorithm settings */ "Algorithm Settings" = "Algoritmeinnstillinger"; +/* Warning text for when alerts are muted */ +"All Alerts Muted" = "Alle varsler er dempet"; + +/* Label for when mute alert will end */ +"All alerts muted until" = "Alle varsler er dempet inntil"; + +/* No comment provided by engineer. */ +"All Favorites" = "Alle favoritter"; + +/* Label for carb quantity entry row on carb entry screen */ +"Amount Consumed" = "Mengde karbohydrater\n(Mengde inntatt)"; + /* The title of the Amplitude service */ "Amplitude" = "Amplitude"; @@ -216,6 +241,9 @@ /* Confirmation message for deleting a CGM */ "Are you sure you want to delete this CGM?" = "Er du sikker på at du vil slette denne CGM?"; +/* No comment provided by engineer. */ +"Are you sure you want to delete this food?" = "Er du sikker på at du vil slette denne maten?"; + /* Confirmation message for deleting a service */ "Are you sure you want to delete this service?" = "Er du sikker på at du vil slette denne tjenesten?"; @@ -303,6 +331,9 @@ /* Description of the prediction input effect for carbohydrates. (1: The glucose unit string) */ "Carbs Absorbed (g) ÷ Carb Ratio (g/U) × Insulin Sensitivity (%1$@/U)" = "Absorberte karbohydrater (g) ÷ Karbforhold (g/E) × insulinfølsomhet ( %1$@ /E)"; +/* No comment provided by engineer. */ +"Caution" = "Forsiktig"; + /* The notification alert describing a low pump battery */ "Change the pump battery immediately" = "Skift pumpebatteriet umiddelbart"; @@ -324,6 +355,9 @@ /* Carb entry section footer text explaining absorption time */ "Choose a longer absorption time for larger meals, or those containing fats and proteins. This is only guidance to the algorithm and need not be exact." = "Velg lengre absorpsjonstid for større måltider, eller de som inneholder fett og proteiner. Dette er kun veiledning til algoritmen og trenger ikke være nøyaktig."; +/* No comment provided by engineer. */ +"Choose Favorite:" = "Velg favoritt:"; + /* Button title to close view The button label of the action used to dismiss the unsafe notification permission alert */ "Close" = "Lukk"; @@ -368,6 +402,9 @@ The title text for the glucose target range schedule */ "Correction Range" = "Korreksjonsområde"; +/* Format string for title of reset loop alert. (1: App name) */ +"Could Not Restart %1$@" = "Kunne ikke starte %1$@ på nytt"; + /* Critical Alerts Status text */ "Critical Alerts" = "Kritiske varsler"; @@ -401,6 +438,9 @@ /* No comment provided by engineer. */ "Delete" = "Slett"; +/* No comment provided by engineer. */ +"Delete “%@”?" = "Slette \"%@\"?"; + /* The title of the button to remove the credentials for a service */ "Delete Account" = "Slett Konto"; @@ -410,6 +450,9 @@ /* Button title to delete CGM */ "Delete CGM" = "Slett CGM"; +/* No comment provided by engineer. */ +"Delete Food" = "Slett mat"; + /* Button title to delete a service */ "Delete Service" = "Slett tjeneste"; @@ -456,6 +499,12 @@ /* Message to the user to enable bluetooth */ "Enable\nBluetooth" = "Aktiver blåtann"; +/* Title for Glucose Based Partial Application toggle */ +"Enable Glucose Based Partial Application" = "Aktiver delvis anvendelse basert på glukose"; + +/* Title for Integral Retrospective Correction toggle */ +"Enable Integral Retrospective Correction" = "Aktivere integrert retrospektiv korrigering"; + /* The action hint of the workout mode toggle button when disabled */ "Enables" = "Aktiverer"; @@ -487,7 +536,7 @@ "Event History" = "Hendelseshistorie"; /* The subtitle format describing eventual glucose. (1: localized glucose value description) */ -"Eventually %@" = "Til slutt %@"; +"Eventually %@" = "Omsider %@"; /* Bolus error description: bolus exceeds maximum bolus in settings. */ "Exceeds maximum allowed bolus in settings" = "Overskrider maksimalt tillatt bolus i innstillingene"; @@ -507,6 +556,9 @@ /* The alert title for a resume error */ "Failed to Resume Insulin Delivery" = "Kunne ikke gjenoppta insulinlevering"; +/* No comment provided by engineer. */ +"FAVORITE FOODS" = "FAVORITTMAT"; + /* Title of insulin model preset */ "Fiasp" = "Fiasp"; @@ -534,6 +586,10 @@ /* The title of the glucose and prediction graph */ "Glucose" = "Blodsukker"; +/* Title for glucose based partial application experiment description + Title of glucose based partial application experiment */ +"Glucose Based Partial Application" = "Partiell anvendelse basert på glukosenivå"; + /* The error message when glucose data is too old to be used. (1: glucose data age in minutes) */ "Glucose data is %1$@ old" = "Blodsukkerdata er %1$@ gammel"; @@ -543,6 +599,9 @@ /* Alert title when glucose data returns while on bolus screen */ "Glucose Data Now Available" = "Blodsukkerdata er utilgjengelig"; +/* Description of the prediction input effect for suspension of insulin delivery */ +"Glucose effect of suspending insulin delivery" = "Glukoseeffekt av å suspendere insulintilførsel"; + /* Alert title for a manual glucose entry out of range error Title for bolus screen warning when glucose entry is out of range */ "Glucose Entry Out of Range" = "Blodsukkerdata er utenfor intervallet"; @@ -559,6 +618,9 @@ /* Immediate Delivery status text */ "Immediate" = "Umiddelbar"; +/* Algorithm Experiments description second paragraph. */ +"In future versions of Loop these experiments may change, end up as standard parts of the Loop Algorithm, or be removed from Loop entirely. Please follow along in the Loop Zulip chat to stay informed of possible changes to these features." = "I fremtidige versjoner av Loop kan disse eksperimentene endres, ende opp som standarddeler av Loop-algoritmen eller fjernes helt fra Loop. Følg med i Loop Zulip-chatten for å holde deg informert om eventuelle endringer i disse funksjonene."; + /* The title of a target alert action specifying an indefinitely long workout targets duration */ "Indefinitely" = "På ubestemt tid"; @@ -597,9 +659,19 @@ /* Insulin type label */ "Insulin Type" = "Insulintype"; +/* Title for integral retrospective correction experiment description + Title of integral retrospective correction experiment */ +"Integral Retrospective Correction" = "Integrert retrospektiv korreksjon"; + +/* Description of Integral Retrospective Correction toggle. */ +"Integral Retrospective Correction (IRC) is an extension of the standard Retrospective Correction (RC) algorithm component in Loop, which adjusts the forecast based on the history of discrepancies between predicted and actual glucose levels.\n\nIn contrast to RC, which looks at discrepancies over the last 30 minutes, with IRC, the history of discrepancies adds up over time. So continued positive discrepancies over time will result in increased dosing. If the discrepancies are negative over time, Loop will reduce dosing further." = "Integral Retrospective Correction (IRC) er en utvidelse av standardalgoritmekomponenten Retrospective Correction (RC) i Loop, som justerer prognosen basert på historikken for avvik mellom forventede og faktiske glukosenivåer.\n\nI motsetning til RC, som ser på avvik i løpet av de siste 30 minuttene, summerer IRC avvikene over tid. Fortsatte positive avvik over tid vil derfor føre til økt dosering. Hvis avvikene er negative over tid, vil Loop redusere doseringen ytterligere."; + /* Description of an interrupted bolus dose entry (1: title for dose type, 2: value (? if no value) in bold, 3: programmed value (? if no value), 4: unit) */ "Interrupted %1$@: %2$@ of %3$@ %4$@" = "Avbrutt %1$@: %2$@ av %3$@ %4$@"; +/* Carb error description: invalid absorption time. (1: Input duration in hours). */ +"Invalid absorption time: %1$@ hours" = "Ugyldig absorpsjonstid: %1$@ timer"; + /* Bolus error description: invalid bolus amount. */ "Invalid Bolus Amount" = "Ugyldig bolusmengde"; @@ -658,6 +730,9 @@ /* The notification alert describing a long-lasting loop failure. The substitution parameter is the time interval since the last loop */ "Loop has not completed successfully in %@" = "Loop har ikke fullført i %@"; +/* Description of Glucose Based Partial Application toggle. */ +"Loop normally gives 40% of your predicted insulin needs each dosing cycle.\n\nWhen the Glucose Based Partial Application experiment is enabled, Loop will vary the percentage of recommended bolus delivered each cycle with glucose level.\n\nNear correction range, it will use 20% (similar to Temp Basal), and gradually increase to a maximum of 80% at high glucose (200 mg/dL, 11.1 mmol/L).\n\nPlease be aware that during fast rising glucose, such as after an unannounced meal, this feature, combined with velocity and retrospective correction effects, may result in a larger dose than your ISF would call for." = "Loop gir normalt 40 %1$ av det forventede insulinbehovet i hver doseringssyklus.\n\nNår eksperimentet Glukosebasert delvis tilførsel er aktivert, vil Loop variere prosentandelen av anbefalt bolus som tilføres hver syklus med glukosenivået.\n\nI nærheten av korreksjonsområdet bruker den 20 %2$ (i likhet med Temp Basal), og øker gradvis til maksimalt 80 %3$ ved høyt glukosenivå (200 mg/dL, 11,1 mmol/L).\n\nVær oppmerksom på at når glukosenivået stiger raskt, f.eks. etter et uanmeldt måltid, kan denne funksjonen, kombinert med hastighets- og retrospektive korreksjonseffekter, resultere i en større dose enn det ISF skulle tilsi."; + /* Description string for automatic bolus dosing strategy */ "Loop will automatically bolus when insulin needs are above scheduled basal, and will use temporary basal rates when needed to reduce insulin delivery below scheduled basal." = "Lopp vil sette bolus når insulinbehovet er over planlagt basal, og vil bruke midlertidige basale rater når det er nødvendig for å redusere insulintilførselen under planlagt basal"; @@ -707,12 +782,16 @@ /* Details for missing data error when momentum effects are missing */ "Momentum effects" = "Momentum effekter"; -/* Text for more info action on notification of upcoming profile expiration */ +/* Text for more info action on notification of upcoming profile expiration + Text for more info action on notification of upcoming TestFlight expiration */ "More Info" = "Mer info"; /* Label for button to mute all alerts */ "Mute All Alerts" = "Demp alle varsler"; +/* Title for mute alert duration selection action sheet */ +"Mute All Alerts Temporarily" = "Slå av alle varsler midlertidig"; + /* Sensor state description for the non-valid state */ "Needs Attention" = "Trenger tilsyn"; @@ -722,6 +801,9 @@ /* The title of the Nightscout service */ "Nightscout" = "Nightscout"; +/* Message for mute alert duration selection action sheet */ +"No alerts or alarms will sound while muted. Select how long you would you like to mute for." = "Ingen varsler eller alarmer vil høres mens de er dempet. Velg hvor lenge du ønsker å dempe lyden."; + /* Title for bolus screen notice when no bolus is recommended Title for bolus screen warning when glucose is below suspend threshold, and a bolus is not recommended Title for bolus screen warning when no bolus is recommended */ @@ -777,6 +859,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "OK"; @@ -807,6 +890,9 @@ /* Format string describing retrospective glucose prediction comparison. (1: Predicted glucose)(2: Actual glucose)(3: difference) */ "Predicted: %1$@\nActual: %2$@ (%3$@)" = "Forventet: %1$@\nFaktisk: %2$@ ( %3$@ )"; +/* Format string describing integral retrospective correction. (1: Integral glucose effect)(2: Total glucose effect) */ +"prediction-description-integral-retrospective-correction" = "prediksjon-beskrivelse-integral-retrospektiv-korreksjon"; + /* Preparing critical event log text */ "Preparing Critical Event Logs" = "Forbereder logg av kritiske hendelser"; @@ -890,15 +976,24 @@ /* The title of the notification action to retry a bolus command */ "Retry" = "Prøv på nytt"; +/* No comment provided by engineer. */ +"Save" = "Lagre"; + /* Button text to save carbs and/or manual glucose entry and deliver a bolus */ "Save and Deliver" = "Lagre og gi bolus"; +/* No comment provided by engineer. */ +"Save as favorite food" = "Lagre som favorittmat"; + /* Button text to save carbs and/or manual glucose entry without a bolus */ "Save without Bolusing" = "Lagre uten å sette bolus"; /* Scheduled Delivery status text */ "Scheduled" = "Planlagt"; +/* No comment provided by engineer. */ +"Selecting a favorite food in the carb entry screen automatically fills in the carb quantity, food type, and absorption time fields! Tap the add button below to create your first favorite food!" = "Når du velger en favorittmat i skjermbildet for innlegging av karbohydrater, fylles feltene for karbohydratmengde, matvaretype og opptakstid automatisk ut! Trykk på knappen Legg til nedenfor for å opprette din første favorittmat!"; + /* The title of the services section in settings */ "Services" = "Tjenester"; @@ -942,6 +1037,9 @@ /* The title text in settings */ "Suspend Threshold" = "Terskel for utsettelse"; +/* Title of the prediction input effect for suspension of insulin delivery */ +"Suspension of Insulin Delivery" = "Suspensjon av insulintilførsel"; + /* Descriptive text for button to add CGM device */ "Tap here to set up a CGM" = "Trykk her for å sette opp en CGM"; @@ -960,6 +1058,12 @@ /* Message presented in the status row instructing the user to tap this row to stop a bolus */ "Tap to Stop" = "Trykk for å stoppe"; +/* Label for button to unmute all alerts */ +"Tap to Unmute Alerts" = "Trykk for å dempe varsler"; + +/* The alert body for unmute alert confirmation */ +"Tap Unmute to resume sound for your alerts and alarms." = "Trykk på Slå av lyd for å gjenoppta lyden for varsler og alarmer."; + /* Alert message for a bolus too small validation error */ "The bolus amount entered is smaller than the minimum deliverable." = "Den angitte bolusmengden er mindre enn minimumsleveransen."; @@ -990,6 +1094,9 @@ /* Title text for button to Therapy Settings */ "Therapy Settings" = "Behandlingsinnstillinger"; +/* String shown when glucose based partial application cannot be enabled because dosing strategy is not set to Automatic Bolus */ +"This option only applies when Loop's Dosing Strategy is set to Automatic Bolus." = "Dette alternativet gjelder bare når Loops doseringsstrategi er satt til Automatisk bolus."; + /* Time Sensitive Status text */ "Time Sensitive Notifications" = "Tidssensitive varsler"; @@ -1032,9 +1139,21 @@ /* Unknown amount of time in settings' profile expiration section */ "Unknown time" = "Ukjent tid"; +/* The title of the action used to unmute alerts */ +"Unmute" = "Oppheve demping"; + +/* The alert title for unmute alert confirmation */ +"Unmute Alerts?" = "Oppheve demping av varsler?"; + +/* Error message when a service can't be found to handle a push notification. (1: Service Identifier) */ +"Unsupported Notification Service: %1$@" = "Varslingstjeneste som ikke støttes: %1$@"; + /* The format for the description of a temporary override end date */ "until %@" = "til %@"; +/* indication of when alerts will be unmuted (1: time when alerts unmute) */ +"Until %1$@" = "Inntil %1$@"; + /* The title of a target alert action specifying pre-meal targets duration for 1 hour or until the user enters carbs (whichever comes first). */ "Until I enter carbs" = "Frem til jeg legger inn karbohydrater"; @@ -1068,6 +1187,9 @@ /* No comment provided by engineer. */ "When out of Closed Loop mode, the app uses a simplified bolus calculator like a typical pump." = "Når den er ute av lukket Loop-modus, bruker appen en forenklet boluskalkulator som en vanlig pumpe."; +/* Format string for message of reset loop alert. (1: App name) (2: error description) */ +"While trying to restart %1$@ an error occured.\n\n%2$@" = "Det oppstod en feil da du prøvde å starte %1$@ på nytt.\n\n%2$@"; + /* The label of the workout mode toggle button */ "Workout Targets" = "Målområder for trening"; diff --git a/Loop/nl.lproj/Localizable.strings b/Loop/nl.lproj/Localizable.strings index 29518ca75c..15a1423c3f 100644 --- a/Loop/nl.lproj/Localizable.strings +++ b/Loop/nl.lproj/Localizable.strings @@ -95,6 +95,9 @@ Description of a bolus dose entry (1: title for dose type, 2: value (? if no value) in bold, 3: unit) */ "%1$@: %2$@ %3$@" = "%1$@: %2$@ %3$@"; +/* No comment provided by engineer. */ +"⚠️" = "⚠️"; + /* Description of the prediction input effect for glucose momentum */ "15 min glucose regression coefficient (b₁), continued with decay over 30 min" = "15 min glucose regressiecoëficiënt (b₁), gevolgd door afbouw over 30 min"; @@ -143,6 +146,9 @@ /* The string format describing active insulin. (1: localized insulin value description) */ "Active Insulin: %@" = "Actieve Insuline: %@"; +/* No comment provided by engineer. */ +"Add a new favorite food" = "Voeg een nieuw favoriet eten toe"; + /* Title of the user activity for adding carbs */ "Add Carb Entry" = "Kh. Inv. Toevoegen"; @@ -171,6 +177,13 @@ Notification & Critical Alert Permissions screen title */ "Alert Permissions" = "Toestemming Meldingen"; +/* Navigation title for algorithms experiments screen + The title of the Algorithm Experiments section in settings */ +"Algorithm Experiments" = "Algoritme Experimenten"; + +/* Algorithm Experiments description. */ +"Algorithm Experiments are optional modifications to the Loop Algorithm. These modifications are less tested than the standard Loop Algorithm, so please use carefully." = "Algoritme Experimenten zijn optionele aanpassingen aan het Loop Algoritme. Deze aanpassingen zijn minder grondig getest dan het standaard Loop Algoritme, dus gebruik het voorzichtig."; + /* The title of the section containing algorithm settings */ "Algorithm Settings" = "Algoritme-instellingen"; @@ -303,6 +316,9 @@ /* Description of the prediction input effect for carbohydrates. (1: The glucose unit string) */ "Carbs Absorbed (g) ÷ Carb Ratio (g/U) × Insulin Sensitivity (%1$@/U)" = "Opgenomen Koolhydraten (g) ÷ Koolhydraatratio (g/E) × Insulinegevoeligheid (%1$@/E)"; +/* No comment provided by engineer. */ +"Caution" = "Voorzichtig"; + /* The notification alert describing a low pump battery */ "Change the pump battery immediately" = "Vervang direct de batterij van de pomp"; @@ -507,6 +523,9 @@ /* The alert title for a resume error */ "Failed to Resume Insulin Delivery" = "Insulinetoediening Hervatten Mislukt"; +/* No comment provided by engineer. */ +"FAVORITE FOODS" = "FAVORIETE ETEN"; + /* Title of insulin model preset */ "Fiasp" = "Fiasp"; @@ -707,7 +726,8 @@ /* Details for missing data error when momentum effects are missing */ "Momentum effects" = "Trendlijneffecten"; -/* Text for more info action on notification of upcoming profile expiration */ +/* Text for more info action on notification of upcoming profile expiration + Text for more info action on notification of upcoming TestFlight expiration */ "More Info" = "Meer Informatie"; /* Label for button to mute all alerts */ @@ -777,6 +797,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "Ok"; @@ -893,12 +914,18 @@ /* Button text to save carbs and/or manual glucose entry and deliver a bolus */ "Save and Deliver" = "Opslaan en Toedienen"; +/* No comment provided by engineer. */ +"Save as favorite food" = "Opslaan als favoriet voedsel"; + /* Button text to save carbs and/or manual glucose entry without a bolus */ "Save without Bolusing" = "Opslaan zonder Bolussen"; /* Scheduled Delivery status text */ "Scheduled" = "Gepland"; +/* No comment provided by engineer. */ +"Selecting a favorite food in the carb entry screen automatically fills in the carb quantity, food type, and absorption time fields! Tap the add button below to create your first favorite food!" = "Het selecteren van een favoriet voedsel in het koolhydraat invoerscherm vult automatisch de velden voor de hoeveelheid koolhydraten, het type voedsel en de absorptietijd in! Tik op de toevoegknop hieronder om je eerste favoriete voedsel te maken!"; + /* The title of the services section in settings */ "Services" = "Services"; diff --git a/Loop/pl.lproj/InfoPlist.strings b/Loop/pl.lproj/InfoPlist.strings index f9c24ced75..d581227b06 100644 --- a/Loop/pl.lproj/InfoPlist.strings +++ b/Loop/pl.lproj/InfoPlist.strings @@ -4,6 +4,9 @@ /* Bundle name */ "CFBundleName" = "$(PRODUCT_NAME)"; +/* Privacy - NFC Scan Usage Description */ +"NFCReaderUsageDescription" = "Aplikacja wykorzystuje NFC do parowania z urządzeniami dla diabetyków."; + /* Privacy - Bluetooth Always Usage Description */ "NSBluetoothAlwaysUsageDescription" = "Bluetooth jest używany do komunikacji z pompą i urządzeniami ciągłego monitoringu glukozy."; diff --git a/Loop/pl.lproj/Localizable.strings b/Loop/pl.lproj/Localizable.strings index 7264f52d10..3a365d7036 100644 --- a/Loop/pl.lproj/Localizable.strings +++ b/Loop/pl.lproj/Localizable.strings @@ -95,6 +95,9 @@ Description of a bolus dose entry (1: title for dose type, 2: value (? if no value) in bold, 3: unit) */ "%1$@: %2$@ %3$@" = "%1$@: %2$@ %3$@"; +/* No comment provided by engineer. */ +"⚠️" = "⚠️"; + /* Description of the prediction input effect for glucose momentum */ "15 min glucose regression coefficient (b₁), continued with decay over 30 min" = "15-minutowy współczynnik regresji glukozy (b₁), kontynuowany z rozkładem przez 30 min."; @@ -143,6 +146,9 @@ /* The string format describing active insulin. (1: localized insulin value description) */ "Active Insulin: %@" = "Aktywna Insulina: %@"; +/* No comment provided by engineer. */ +"Add a new favorite food" = "Dodaj nowe ulubione jedzenie"; + /* Title of the user activity for adding carbs */ "Add Carb Entry" = "Wprowadź węglowodany"; @@ -171,9 +177,28 @@ Notification & Critical Alert Permissions screen title */ "Alert Permissions" = "Uprawnienia alertów"; +/* Navigation title for algorithms experiments screen + The title of the Algorithm Experiments section in settings */ +"Algorithm Experiments" = "Algorytmy Eksperymentalne"; + +/* Algorithm Experiments description. */ +"Algorithm Experiments are optional modifications to the Loop Algorithm. These modifications are less tested than the standard Loop Algorithm, so please use carefully." = "Eksperymenty algorytmiczne to opcjonalne modyfikacje algorytmu pętli. Te modyfikacje są mniej przetestowane niż standardowy algorytm pętli, więc używaj ich ostrożnie."; + /* The title of the section containing algorithm settings */ "Algorithm Settings" = "Ustawienia algorytmu"; +/* Warning text for when alerts are muted */ +"All Alerts Muted" = "Wszystkie alerty wyciszone"; + +/* Label for when mute alert will end */ +"All alerts muted until" = "Wszystkie alerty wyciszono do"; + +/* No comment provided by engineer. */ +"All Favorites" = "Wszystkie ulubione"; + +/* Label for carb quantity entry row on carb entry screen */ +"Amount Consumed" = "Ilość węglowodanów"; + /* The title of the Amplitude service */ "Amplitude" = "Amplituda"; @@ -216,6 +241,9 @@ /* Confirmation message for deleting a CGM */ "Are you sure you want to delete this CGM?" = "Czy na pewno chcesz usunąć ten CGM?"; +/* No comment provided by engineer. */ +"Are you sure you want to delete this food?" = "Czy na pewno chcesz usunąć to jedzenie?"; + /* Confirmation message for deleting a service */ "Are you sure you want to delete this service?" = "Czy na pewno chcesz usunąć tę usługę?"; @@ -303,6 +331,9 @@ /* Description of the prediction input effect for carbohydrates. (1: The glucose unit string) */ "Carbs Absorbed (g) ÷ Carb Ratio (g/U) × Insulin Sensitivity (%1$@/U)" = "Ilość węglowodanów (g) ÷ stosunek węglowodanów (g/J) × czułość insuliny (%1$@/J)"; +/* No comment provided by engineer. */ +"Caution" = "Uwaga"; + /* The notification alert describing a low pump battery */ "Change the pump battery immediately" = "Natychmiast wymienić baterię pompy"; @@ -324,6 +355,9 @@ /* Carb entry section footer text explaining absorption time */ "Choose a longer absorption time for larger meals, or those containing fats and proteins. This is only guidance to the algorithm and need not be exact." = "Wybierz dłuższy czas absorpcji dla większych, bogatobiałkowych lub wysokotłuszczowych posiłków. To tylko wskazówka dla algorytmu i nie musi być bardzo dokładna."; +/* No comment provided by engineer. */ +"Choose Favorite:" = "Wybierz ulubione:"; + /* Button title to close view The button label of the action used to dismiss the unsafe notification permission alert */ "Close" = "Zamknij"; @@ -368,6 +402,9 @@ The title text for the glucose target range schedule */ "Correction Range" = "Zakres docelowy"; +/* Format string for title of reset loop alert. (1: App name) */ +"Could Not Restart %1$@" = "Nie można ponownie uruchomić %1$@"; + /* Critical Alerts Status text */ "Critical Alerts" = "Alerty krytyczne"; @@ -401,6 +438,9 @@ /* No comment provided by engineer. */ "Delete" = "Usunąć"; +/* No comment provided by engineer. */ +"Delete “%@”?" = "Usunąć „ %@ ”?"; + /* The title of the button to remove the credentials for a service */ "Delete Account" = "Usuń konto"; @@ -410,6 +450,9 @@ /* Button title to delete CGM */ "Delete CGM" = "Usuń CGM"; +/* No comment provided by engineer. */ +"Delete Food" = "Usuń jedzenie"; + /* Button title to delete a service */ "Delete Service" = "Usuń usługę"; @@ -456,6 +499,12 @@ /* Message to the user to enable bluetooth */ "Enable\nBluetooth" = "Włączać\nBluetooth"; +/* Title for Glucose Based Partial Application toggle */ +"Enable Glucose Based Partial Application" = "Włącz Algorytm adaptacyjny"; + +/* Title for Integral Retrospective Correction toggle */ +"Enable Integral Retrospective Correction" = "Włącz Integralną Korektę Retrospektywną (IRC)"; + /* The action hint of the workout mode toggle button when disabled */ "Enables" = "Włącza"; @@ -507,6 +556,9 @@ /* The alert title for a resume error */ "Failed to Resume Insulin Delivery" = "Nie udało się wznowić podawania insuliny"; +/* No comment provided by engineer. */ +"FAVORITE FOODS" = "ULUBIONE JEDZENIE"; + /* Title of insulin model preset */ "Fiasp" = "Fiasp"; @@ -534,6 +586,10 @@ /* The title of the glucose and prediction graph */ "Glucose" = "Glukoza"; +/* Title for glucose based partial application experiment description + Title of glucose based partial application experiment */ +"Glucose Based Partial Application" = "Algorytm adaptacyjny"; + /* The error message when glucose data is too old to be used. (1: glucose data age in minutes) */ "Glucose data is %1$@ old" = "Dane o glukozie są nieaktualne od %1$@"; @@ -543,6 +599,9 @@ /* Alert title when glucose data returns while on bolus screen */ "Glucose Data Now Available" = "Dane dotyczące glukozy są już dostępne"; +/* Description of the prediction input effect for suspension of insulin delivery */ +"Glucose effect of suspending insulin delivery" = "Wpływ wstrzymania podawania insuliny na poziom glukozy"; + /* Alert title for a manual glucose entry out of range error Title for bolus screen warning when glucose entry is out of range */ "Glucose Entry Out of Range" = "Wprowadzanie glukoza jest poza zakresem"; @@ -559,6 +618,9 @@ /* Immediate Delivery status text */ "Immediate" = "Natychmiastowy"; +/* Algorithm Experiments description second paragraph. */ +"In future versions of Loop these experiments may change, end up as standard parts of the Loop Algorithm, or be removed from Loop entirely. Please follow along in the Loop Zulip chat to stay informed of possible changes to these features." = "W przyszłych wersjach Loop te eksperymenty mogą się zmienić, stać się standardowymi częściami algorytmu Loop lub zostać całkowicie usunięte z Loop. Śledź czat Loop Zulip, aby być na bieżąco z możliwymi zmianami w tych funkcjach."; + /* The title of a target alert action specifying an indefinitely long workout targets duration */ "Indefinitely" = "Niemożliwy do określenia"; @@ -597,9 +659,19 @@ /* Insulin type label */ "Insulin Type" = "Rodzaj insuliny"; +/* Title for integral retrospective correction experiment description + Title of integral retrospective correction experiment */ +"Integral Retrospective Correction" = "Integralna korekta retrospektywna"; + +/* Description of Integral Retrospective Correction toggle. */ +"Integral Retrospective Correction (IRC) is an extension of the standard Retrospective Correction (RC) algorithm component in Loop, which adjusts the forecast based on the history of discrepancies between predicted and actual glucose levels.\n\nIn contrast to RC, which looks at discrepancies over the last 30 minutes, with IRC, the history of discrepancies adds up over time. So continued positive discrepancies over time will result in increased dosing. If the discrepancies are negative over time, Loop will reduce dosing further." = "Integralna korekta retrospektywna (IRC) jest rozszerzeniem standardowego komponentu algorytmu Korekta retrospektywna (RC) w Loop, który koryguje prognozę na podstawie historii rozbieżności między przewidywanymi a rzeczywistymi poziomami glukozy. \n\n W przeciwieństwie do RC, który analizuje rozbieżności w ciągu ostatnich 30 minut, w przypadku IRC historia rozbieżności sumuje się w czasie. Tak więc utrzymujące się dodatnie rozbieżności w czasie spowodują zwiększenie dawki. Jeśli rozbieżności są ujemne w czasie, Loop jeszcze bardziej zmniejszy podawanie insuliny."; + /* Description of an interrupted bolus dose entry (1: title for dose type, 2: value (? if no value) in bold, 3: programmed value (? if no value), 4: unit) */ "Interrupted %1$@: %2$@ of %3$@ %4$@" = "Przerwane %1$@ : %2$@ z %3$@ %4$@"; +/* Carb error description: invalid absorption time. (1: Input duration in hours). */ +"Invalid absorption time: %1$@ hours" = "Nieprawidłowy czas absorpcji: %1$@ godz"; + /* Bolus error description: invalid bolus amount. */ "Invalid Bolus Amount" = "Nieprawidłowa wielkość bolusa"; @@ -658,6 +730,9 @@ /* The notification alert describing a long-lasting loop failure. The substitution parameter is the time interval since the last loop */ "Loop has not completed successfully in %@" = "Loop nie działał poprawnie przez %@"; +/* Description of Glucose Based Partial Application toggle. */ +"Loop normally gives 40% of your predicted insulin needs each dosing cycle.\n\nWhen the Glucose Based Partial Application experiment is enabled, Loop will vary the percentage of recommended bolus delivered each cycle with glucose level.\n\nNear correction range, it will use 20% (similar to Temp Basal), and gradually increase to a maximum of 80% at high glucose (200 mg/dL, 11.1 mmol/L).\n\nPlease be aware that during fast rising glucose, such as after an unannounced meal, this feature, combined with velocity and retrospective correction effects, may result in a larger dose than your ISF would call for." = "Pętla zwykle daje 40%1$ przewidywanego zapotrzebowania na insulinę w każdym cyklu dawkowania. \n\nPo włączeniu eksperymentu częściowego podania insuliny w oparciu o glukozę, Loop będzie zmieniać procent zalecanego bolusa podawanego w każdym cyklu w zależności od poziomu glukozy. \n\nW pobliżu zakresu korekcji będzie zużywać 20%2$ (podobnie jak Baza Tymczasowa) i stopniowo zwiększać do maksimum 80%3$ przy wysokim stężeniu glukozy (200 mg/dl, 11,1 mmol/l). \n\nNależy pamiętać, że podczas szybkiego wzrostu stężenia glukozy, na przykład po niezapowiedzianym posiłku, ta funkcja w połączeniu z szybkością i retrospektywnymi efektami korekcyjnymi może skutkować większą dawką, niż wymagałby ISF."; + /* Description string for automatic bolus dosing strategy */ "Loop will automatically bolus when insulin needs are above scheduled basal, and will use temporary basal rates when needed to reduce insulin delivery below scheduled basal." = "Pętla automatycznie poda bolusa, kiedy zapotrzebowanie na insulinę przekroczy zaplanowaną dawkę podstawową, a w razie potrzeby zredukuje zaplanowaną dawkę podstawową (bazę)."; @@ -707,12 +782,16 @@ /* Details for missing data error when momentum effects are missing */ "Momentum effects" = "wpływ pędu"; -/* Text for more info action on notification of upcoming profile expiration */ +/* Text for more info action on notification of upcoming profile expiration + Text for more info action on notification of upcoming TestFlight expiration */ "More Info" = "Więcej informacji"; /* Label for button to mute all alerts */ "Mute All Alerts" = "Wycisz wszystkie alerty"; +/* Title for mute alert duration selection action sheet */ +"Mute All Alerts Temporarily" = "Tymczasowo wycisz wszystkie alerty"; + /* Sensor state description for the non-valid state */ "Needs Attention" = "Potrzebuje uwagi"; @@ -722,6 +801,9 @@ /* The title of the Nightscout service */ "Nightscout" = "Nightscout"; +/* Message for mute alert duration selection action sheet */ +"No alerts or alarms will sound while muted. Select how long you would you like to mute for." = "Po wyciszeniu nie będą emitowane żadne alerty ani alarmy. Wybierz, jak długo chcesz wyciszyć."; + /* Title for bolus screen notice when no bolus is recommended Title for bolus screen warning when glucose is below suspend threshold, and a bolus is not recommended Title for bolus screen warning when no bolus is recommended */ @@ -777,6 +859,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "OK"; @@ -807,6 +890,9 @@ /* Format string describing retrospective glucose prediction comparison. (1: Predicted glucose)(2: Actual glucose)(3: difference) */ "Predicted: %1$@\nActual: %2$@ (%3$@)" = "Przewidywana: %1$@Rzeczywista: %2$@ (%3$@)"; +/* Format string describing integral retrospective correction. (1: Integral glucose effect)(2: Total glucose effect) */ +"prediction-description-integral-retrospective-correction" = "predykcja-opis-całka-retrospektywna-korekta"; + /* Preparing critical event log text */ "Preparing Critical Event Logs" = "Przygotowywanie dzienników zdarzeń krytycznych"; @@ -890,15 +976,24 @@ /* The title of the notification action to retry a bolus command */ "Retry" = "Spróbuj ponownie"; +/* No comment provided by engineer. */ +"Save" = "Zapisz"; + /* Button text to save carbs and/or manual glucose entry and deliver a bolus */ "Save and Deliver" = "Zapisz i podaj"; +/* No comment provided by engineer. */ +"Save as favorite food" = "Zapisz jako ulubione jedzenie"; + /* Button text to save carbs and/or manual glucose entry without a bolus */ "Save without Bolusing" = "Zapisz bez podania Bolusa"; /* Scheduled Delivery status text */ "Scheduled" = "Zaplanowane"; +/* No comment provided by engineer. */ +"Selecting a favorite food in the carb entry screen automatically fills in the carb quantity, food type, and absorption time fields! Tap the add button below to create your first favorite food!" = "Wybór ulubionego jedzenia na ekranie wprowadzania węglowodanów powoduje automatyczne wypełnienie pól ilości węglowodanów, rodzaju jedzenia i czasu wchłaniania! Dotknij przycisku dodawania poniżej, aby stworzyć swoje pierwsze ulubione jedzenie!"; + /* The title of the services section in settings */ "Services" = "Usługi"; @@ -942,6 +1037,9 @@ /* The title text in settings */ "Suspend Threshold" = "Próg zawieszenia pompy"; +/* Title of the prediction input effect for suspension of insulin delivery */ +"Suspension of Insulin Delivery" = "Wstrzymanie podawania insuliny"; + /* Descriptive text for button to add CGM device */ "Tap here to set up a CGM" = "Stuknij tutaj, aby skonfigurować CGM"; @@ -960,6 +1058,12 @@ /* Message presented in the status row instructing the user to tap this row to stop a bolus */ "Tap to Stop" = "Bolus STOP!"; +/* Label for button to unmute all alerts */ +"Tap to Unmute Alerts" = "Stuknij, aby wyłączyć wyciszenie alertów"; + +/* The alert body for unmute alert confirmation */ +"Tap Unmute to resume sound for your alerts and alarms." = "Stuknij opcję Wyłącz wyciszenie, aby wznowić dźwięk alertów i alarmów."; + /* Alert message for a bolus too small validation error */ "The bolus amount entered is smaller than the minimum deliverable." = "Wprowadzona wielkość bolusa jest mniejsza niż minimalna możliwa do podania."; @@ -990,6 +1094,9 @@ /* Title text for button to Therapy Settings */ "Therapy Settings" = "Ustawienia terapii"; +/* String shown when glucose based partial application cannot be enabled because dosing strategy is not set to Automatic Bolus */ +"This option only applies when Loop's Dosing Strategy is set to Automatic Bolus." = "Ta opcja ma zastosowanie tylko wtedy, gdy Strategia dawkowania pętli jest ustawiona na Automatyczny bolus."; + /* Time Sensitive Status text */ "Time Sensitive Notifications" = "Powiadomienia zależne od czasu"; @@ -1032,9 +1139,21 @@ /* Unknown amount of time in settings' profile expiration section */ "Unknown time" = "Nieznany czas"; +/* The title of the action used to unmute alerts */ +"Unmute" = "Wyłącz wyciszenie"; + +/* The alert title for unmute alert confirmation */ +"Unmute Alerts?" = "Wyciszyć Alerty?"; + +/* Error message when a service can't be found to handle a push notification. (1: Service Identifier) */ +"Unsupported Notification Service: %1$@" = "Nieobsługiwana usługa powiadomień: %1$@"; + /* The format for the description of a temporary override end date */ "until %@" = "do %@"; +/* indication of when alerts will be unmuted (1: time when alerts unmute) */ +"Until %1$@" = "Do %1$@"; + /* The title of a target alert action specifying pre-meal targets duration for 1 hour or until the user enters carbs (whichever comes first). */ "Until I enter carbs" = "Dopóki nie wprowadzę węglowodanów"; @@ -1068,6 +1187,9 @@ /* No comment provided by engineer. */ "When out of Closed Loop mode, the app uses a simplified bolus calculator like a typical pump." = "Poza trybem pętli zamkniętej aplikacja korzysta z uproszczonego kalkulatora bolusa, takiego jak typowa pompa."; +/* Format string for message of reset loop alert. (1: App name) (2: error description) */ +"While trying to restart %1$@ an error occured.\n\n%2$@" = "Podczas próby ponownego uruchomienia %1$@ wystąpił błąd. \n\n %2$@"; + /* The label of the workout mode toggle button */ "Workout Targets" = "Zakres w czasie wysiłku fizycznego"; diff --git a/Loop/pt-BR.lproj/Localizable.strings b/Loop/pt-BR.lproj/Localizable.strings index d3d565ca81..cb0345df80 100644 --- a/Loop/pt-BR.lproj/Localizable.strings +++ b/Loop/pt-BR.lproj/Localizable.strings @@ -331,7 +331,8 @@ /* Details for missing data error when momentum effects are missing */ "Momentum effects" = "Efeitos de aceleração"; -/* Text for more info action on notification of upcoming profile expiration */ +/* Text for more info action on notification of upcoming profile expiration + Text for more info action on notification of upcoming TestFlight expiration */ "More Info" = "Mais Info"; /* Sensor state description for the non-valid state */ @@ -348,6 +349,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "OK"; diff --git a/Loop/ro.lproj/Localizable.strings b/Loop/ro.lproj/Localizable.strings index bec9c5c23d..3a0da3f688 100644 --- a/Loop/ro.lproj/Localizable.strings +++ b/Loop/ro.lproj/Localizable.strings @@ -707,7 +707,8 @@ /* Details for missing data error when momentum effects are missing */ "Momentum effects" = "Efecte momentum"; -/* Text for more info action on notification of upcoming profile expiration */ +/* Text for more info action on notification of upcoming profile expiration + Text for more info action on notification of upcoming TestFlight expiration */ "More Info" = "Detalii"; /* Label for button to mute all alerts */ @@ -777,6 +778,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "OK"; diff --git a/Loop/ru.lproj/Localizable.strings b/Loop/ru.lproj/Localizable.strings index ded27d134b..43cb4146cb 100644 --- a/Loop/ru.lproj/Localizable.strings +++ b/Loop/ru.lproj/Localizable.strings @@ -95,6 +95,9 @@ Description of a bolus dose entry (1: title for dose type, 2: value (? if no value) in bold, 3: unit) */ "%1$@: %2$@ %3$@" = "%1$@: %2$@ %3$@"; +/* No comment provided by engineer. */ +"⚠️" = "⚠️"; + /* Description of the prediction input effect for glucose momentum */ "15 min glucose regression coefficient (b₁), continued with decay over 30 min" = "15-мин коэффициент регрессии гликемии (b1), продолжен с угасанием 30 мин"; @@ -707,7 +710,8 @@ /* Details for missing data error when momentum effects are missing */ "Momentum effects" = "Влияние динамики СК"; -/* Text for more info action on notification of upcoming profile expiration */ +/* Text for more info action on notification of upcoming profile expiration + Text for more info action on notification of upcoming TestFlight expiration */ "More Info" = "Доп. инфо"; /* Label for button to mute all alerts */ @@ -777,6 +781,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "OK"; @@ -807,6 +812,9 @@ /* Format string describing retrospective glucose prediction comparison. (1: Predicted glucose)(2: Actual glucose)(3: difference) */ "Predicted: %1$@\nActual: %2$@ (%3$@)" = "Прогноз: %1$@\nФакт: %2$@ (%3$@)"; +/* Format string describing integral retrospective correction. (1: Integral glucose effect)(2: Total glucose effect) */ +"prediction-description-integral-retrospective-correction" = "описание прогнозирования с помощью интегральной ретроспективной коррекции"; + /* Preparing critical event log text */ "Preparing Critical Event Logs" = "Подготовка логов критических событий"; @@ -990,6 +998,9 @@ /* Title text for button to Therapy Settings */ "Therapy Settings" = "Настройки терапии"; +/* String shown when glucose based partial application cannot be enabled because dosing strategy is not set to Automatic Bolus */ +"This option only applies when Loop's Dosing Strategy is set to Automatic Bolus." = "Эта опция применима только в том случае, если для стратегии дозирования петли установлено значение «Автоматический болюс»."; + /* Time Sensitive Status text */ "Time Sensitive Notifications" = "Уведомления, чувствительные к времени"; @@ -1032,9 +1043,21 @@ /* Unknown amount of time in settings' profile expiration section */ "Unknown time" = "Неизвестное время"; +/* The title of the action used to unmute alerts */ +"Unmute" = "Включить звук"; + +/* The alert title for unmute alert confirmation */ +"Unmute Alerts?" = "Включить звук оповещений?"; + +/* Error message when a service can't be found to handle a push notification. (1: Service Identifier) */ +"Unsupported Notification Service: %1$@" = "Неподдерживаемая служба уведомлений: %1$@"; + /* The format for the description of a temporary override end date */ "until %@" = "до %@"; +/* indication of when alerts will be unmuted (1: time when alerts unmute) */ +"Until %1$@" = "До %1$@"; + /* The title of a target alert action specifying pre-meal targets duration for 1 hour or until the user enters carbs (whichever comes first). */ "Until I enter carbs" = "Пока я не введу углеводы"; @@ -1068,6 +1091,9 @@ /* No comment provided by engineer. */ "When out of Closed Loop mode, the app uses a simplified bolus calculator like a typical pump." = "Когда приложение выходит из режима замкнутого цикла, оно использует упрощенный калькулятор болюса, как в обычной помпе."; +/* Format string for message of reset loop alert. (1: App name) (2: error description) */ +"While trying to restart %1$@ an error occured.\n\n%2$@" = "При попытке перезапустить %1$@ произошла ошибка. \n\n %2$@"; + /* The label of the workout mode toggle button */ "Workout Targets" = "Целевые значения при физической нагрузке"; diff --git a/Loop/sk.lproj/Localizable.strings b/Loop/sk.lproj/Localizable.strings index e0820a4f00..a1bbaec38d 100644 --- a/Loop/sk.lproj/Localizable.strings +++ b/Loop/sk.lproj/Localizable.strings @@ -231,6 +231,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "OK"; diff --git a/Loop/sv.lproj/Localizable.strings b/Loop/sv.lproj/Localizable.strings index 838ee70ed2..ceec268207 100644 --- a/Loop/sv.lproj/Localizable.strings +++ b/Loop/sv.lproj/Localizable.strings @@ -543,7 +543,8 @@ /* Details for missing data error when momentum effects are missing */ "Momentum effects" = "Momentumeffekter"; -/* Text for more info action on notification of upcoming profile expiration */ +/* Text for more info action on notification of upcoming profile expiration + Text for more info action on notification of upcoming TestFlight expiration */ "More Info" = "Mer info"; /* Sensor state description for the non-valid state */ @@ -583,6 +584,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "OK"; diff --git a/Loop/tr.lproj/Localizable.strings b/Loop/tr.lproj/Localizable.strings index a3da653f5c..43663a02b4 100644 --- a/Loop/tr.lproj/Localizable.strings +++ b/Loop/tr.lproj/Localizable.strings @@ -707,7 +707,8 @@ /* Details for missing data error when momentum effects are missing */ "Momentum effects" = "Momentum etkileri"; -/* Text for more info action on notification of upcoming profile expiration */ +/* Text for more info action on notification of upcoming profile expiration + Text for more info action on notification of upcoming TestFlight expiration */ "More Info" = "Daha fazla bilgi"; /* Label for button to mute all alerts */ @@ -777,6 +778,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "Tamam"; diff --git a/Loop/vi.lproj/Localizable.strings b/Loop/vi.lproj/Localizable.strings index b3935045e6..1ee202e94e 100644 --- a/Loop/vi.lproj/Localizable.strings +++ b/Loop/vi.lproj/Localizable.strings @@ -331,7 +331,8 @@ /* Details for missing data error when momentum effects are missing */ "Momentum effects" = "Hiệu ứng động lượng"; -/* Text for more info action on notification of upcoming profile expiration */ +/* Text for more info action on notification of upcoming profile expiration + Text for more info action on notification of upcoming TestFlight expiration */ "More Info" = "Thêm thông tin"; /* Sensor state description for the non-valid state */ @@ -348,6 +349,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "OK"; diff --git a/LoopUI/nb.lproj/Localizable.strings b/LoopUI/nb.lproj/Localizable.strings index cf8aa03166..eb5ce50cb9 100644 --- a/LoopUI/nb.lproj/Localizable.strings +++ b/LoopUI/nb.lproj/Localizable.strings @@ -1,14 +1,14 @@ /* Green closed loop ON message (1: last loop string) (2: app name) */ -"\n%1$@\n\n%2$@ is operating with Closed Loop in the ON position." = "\n%1$@\n\n%2$@ opererer med Closed Loop i ON-posisjon."; +"\n%1$@\n\n%2$@ is operating with Closed Loop in the ON position." = "%1$@\n\n%2$@ opererer med Closed Loop i ON-posisjon."; /* Red loop message (1: last loop string) (2: app name) */ -"\n%1$@\n\nTap your CGM and insulin pump status icons for more information. %2$@ will continue trying to complete a loop, but check for potential communication issues with your pump and CGM." = "\n%1$@\n\n Trykk på statusikonene for CGM og insulinpumpe for mer informasjon. %2$@ vil fortsette å prøve å fullføre en sløyfe, men se etter potensielle kommunikasjonsproblemer med pumpen og CGM."; +"\n%1$@\n\nTap your CGM and insulin pump status icons for more information. %2$@ will continue trying to complete a loop, but check for potential communication issues with your pump and CGM." = "%1$@\n\n Trykk på statusikonene for CGM og insulinpumpe for mer informasjon. %2$@ vil fortsette å prøve å fullføre en sløyfe, men se etter potensielle kommunikasjonsproblemer med pumpen og CGM."; /* Yellow loop message (1: last loop string) (2: app name) */ -"\n%1$@\n\nTap your CGM and insulin pump status icons for more information. %2$@ will continue trying to complete a loop, but watch for potential communication issues with your pump and CGM." = "\n%1$@\n\nTrykk på statusikonene for CGM og insulinpumpe for mer informasjon. %2$@ vil fortsette å prøve å fullføre en loop, men se etter potensielle kommunikasjonsproblemer med pumpen og CGM."; +"\n%1$@\n\nTap your CGM and insulin pump status icons for more information. %2$@ will continue trying to complete a loop, but watch for potential communication issues with your pump and CGM." = "%1$@\n\nTrykk på statusikonene for CGM og insulinpumpe for mer informasjon. %2$@ vil fortsette å prøve å fullføre en loop, men se etter potensielle kommunikasjonsproblemer med pumpen og CGM."; /* Green closed loop OFF message (1: app name)(2: reason for open loop) */ -"\n%1$@ is operating with Closed Loop in the OFF position. Your pump and CGM will continue operating, but the app will not adjust dosing automatically.\n\n%2$@" = "\n%1$@ opererer med Closed Loop i OFF posisjon. Pumpen og CGM vil fortsette å fungere, men appen vil ikke justere doseringen automatisk.\n\n%2$@"; +"\n%1$@ is operating with Closed Loop in the OFF position. Your pump and CGM will continue operating, but the app will not adjust dosing automatically.\n\n%2$@" = "%1$@ opererer med Closed Loop i OFF posisjon. Pumpen og CGM vil fortsette å fungere, men appen vil ikke justere doseringen automatisk.\n\n%2$@"; /* No glucose value representation (3 dashes for mg/dL) */ "– – –" = "– – –"; diff --git a/WatchApp Extension/it.lproj/Localizable.strings b/WatchApp Extension/it.lproj/Localizable.strings index 49989cb3fa..2ec6d8bdc3 100644 --- a/WatchApp Extension/it.lproj/Localizable.strings +++ b/WatchApp Extension/it.lproj/Localizable.strings @@ -11,7 +11,7 @@ "%1$@ – %2$@ %3$@" = "%1$@ – %2$@ %3$@"; /* HUD row title for COB */ -"Active Carbs" = "Carb Attivi"; +"Active Carbs" = "Carboidrati Attivi"; /* HUD row title for IOB */ "Active Insulin" = "Insulina attiva"; @@ -118,5 +118,5 @@ "Unable to Reach iPhone" = "Impossibile raggiungere iPhone"; /* The text for the Watch button for enabling workout mode */ -"Workout" = "Allenarsi"; +"Workout" = "Allenamento"; diff --git a/WatchApp Extension/pl.lproj/Localizable.strings b/WatchApp Extension/pl.lproj/Localizable.strings index c8d0ba1465..a11ca5bd91 100644 --- a/WatchApp Extension/pl.lproj/Localizable.strings +++ b/WatchApp Extension/pl.lproj/Localizable.strings @@ -69,7 +69,7 @@ "On" = "Włącz"; /* The text for the Watch button for enabling a temporary override */ -"Override" = "Pominięcie"; +"Override" = "Cel Tymczasowy"; /* Alert message for updated bolus recommendation on Apple Watch */ "Please reconfirm the bolus amount." = "Potwierdź ponownie wielkość bolusa."; diff --git a/WatchApp/pl.lproj/Interface.strings b/WatchApp/pl.lproj/Interface.strings index 6199faad1e..341bbf4aeb 100644 --- a/WatchApp/pl.lproj/Interface.strings +++ b/WatchApp/pl.lproj/Interface.strings @@ -29,7 +29,7 @@ "MZU-QV-PtZ.text" = "TYTUŁ"; /* Class = "WKInterfaceLabel"; text = "Override"; ObjectID = "nC0-X3-oFJ"; */ -"nC0-X3-oFJ.text" = "Pominięcie"; +"nC0-X3-oFJ.text" = "Cel Tymczasowy"; /* Class = "WKInterfaceController"; title = "Loop"; ObjectID = "rNf-Mh-tID"; */ "rNf-Mh-tID.title" = "Loop"; From 7c60cacb2e9490db21e60cedb4a4c4586b65074a Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Sun, 22 Oct 2023 14:06:55 -0500 Subject: [PATCH 41/49] Enable pump heartbeat if needed when adding a pump (#2058) --- Loop/Managers/DeviceDataManager.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index b6cd35d3a6..2751f18f50 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -752,6 +752,8 @@ private extension DeviceDataManager { deliveryUncertaintyAlertManager = DeliveryUncertaintyAlertManager(pumpManager: pumpManager, alertPresenter: alertPresenter) analyticsServicesManager.identifyPumpType(pumpManager.pluginIdentifier) + + updatePumpManagerBLEHeartbeatPreference() } } From ea25a86bda78aaddb3c98e8b0a3942a4a956d198 Mon Sep 17 00:00:00 2001 From: Marion Barker Date: Sat, 28 Oct 2023 08:41:39 -0700 Subject: [PATCH 42/49] use LoopConstants instead of hard-coded time intervals (#2050) --- Loop/View Models/CarbEntryViewModel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Loop/View Models/CarbEntryViewModel.swift b/Loop/View Models/CarbEntryViewModel.swift index f271793d1c..261c529ed5 100644 --- a/Loop/View Models/CarbEntryViewModel.swift +++ b/Loop/View Models/CarbEntryViewModel.swift @@ -59,10 +59,10 @@ final class CarbEntryViewModel: ObservableObject { @Published var time = Date() private var date = Date() var minimumDate: Date { - get { date.addingTimeInterval(.hours(-12)) } + get { date.addingTimeInterval(LoopConstants.maxCarbEntryPastTime) } } var maximumDate: Date { - get { date.addingTimeInterval(.hours(1)) } + get { date.addingTimeInterval(LoopConstants.maxCarbEntryFutureTime) } } @Published var foodType = "" From b63c4928e3f7fbe50b25374da2d2d5980854d4d3 Mon Sep 17 00:00:00 2001 From: Marion Barker Date: Sun, 10 Dec 2023 09:57:05 -0500 Subject: [PATCH 43/49] Probably typo (#2096) --- Loop/Managers/RemoteDataServicesManager.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Loop/Managers/RemoteDataServicesManager.swift b/Loop/Managers/RemoteDataServicesManager.swift index 296e3befa9..bf21376bc3 100644 --- a/Loop/Managers/RemoteDataServicesManager.swift +++ b/Loop/Managers/RemoteDataServicesManager.swift @@ -561,13 +561,13 @@ extension RemoteDataServicesManager { self.cgmEventStore.executeCgmEventQuery(fromQueryAnchor: previousQueryAnchor) { result in switch result { case .failure(let error): - self.log.error("Error querying pump event data: %{public}@", String(describing: error)) + self.log.error("Error querying cgm event data: %{public}@", String(describing: error)) semaphore.signal() case .success(let queryAnchor, let data): remoteDataService.uploadCgmEventData(data) { result in switch result { case .failure(let error): - self.log.error("Error synchronizing pump event data: %{public}@", String(describing: error)) + self.log.error("Error synchronizing cgm event data: %{public}@", String(describing: error)) self.uploadFailed(key) case .success: UserDefaults.appGroup?.setQueryAnchor(for: remoteDataService, withRemoteDataType: .cgmEvent, queryAnchor) From b6610a1d44878e96b7898ebafbf40f2b6b4a6560 Mon Sep 17 00:00:00 2001 From: Marion Barker Date: Mon, 11 Dec 2023 20:47:57 -0500 Subject: [PATCH 44/49] fix logic error, testflight expiration is independent of profile expiration (#2097) --- Loop/Managers/AppExpirationAlerter.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Loop/Managers/AppExpirationAlerter.swift b/Loop/Managers/AppExpirationAlerter.swift index eee1d90f4f..d5dd84518f 100644 --- a/Loop/Managers/AppExpirationAlerter.swift +++ b/Loop/Managers/AppExpirationAlerter.swift @@ -152,7 +152,7 @@ class AppExpirationAlerter { if isTestFlight, let buildDate = buildDate() { let testflightExpiration = Calendar.current.date(byAdding: .day, value: 90, to: buildDate)! - return profileExpiration < testflightExpiration ? profileExpiration : testflightExpiration + return testflightExpiration } else { return profileExpiration } From 7179901e8729e552cb7d527c75ef22e2bd867287 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Sun, 18 Feb 2024 11:28:25 -0600 Subject: [PATCH 45/49] Updates to tests for iOS 17 (#2080) --- Loop/Managers/Alerts/StoredAlert.swift | 16 ++++++++++++--- .../Managers/Alerts/AlertStoreTests.swift | 20 +++++++++---------- .../Managers/Alerts/StoredAlertTests.swift | 12 +++++------ .../ViewModels/BolusEntryViewModelTests.swift | 4 ++-- 4 files changed, 31 insertions(+), 21 deletions(-) diff --git a/Loop/Managers/Alerts/StoredAlert.swift b/Loop/Managers/Alerts/StoredAlert.swift index fb5b431074..39ecc0a041 100644 --- a/Loop/Managers/Alerts/StoredAlert.swift +++ b/Loop/Managers/Alerts/StoredAlert.swift @@ -12,9 +12,19 @@ import UIKit extension StoredAlert { - static var encoder = JSONEncoder() - static var decoder = JSONDecoder() - + static let encoder: JSONEncoder = { + let encoder = JSONEncoder() + encoder.outputFormatting = [.sortedKeys] + encoder.dateEncodingStrategy = .iso8601 + return encoder + }() + + static let decoder: JSONDecoder = { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + return decoder + }() + convenience init(from alert: Alert, context: NSManagedObjectContext, issuedDate: Date = Date(), syncIdentifier: UUID = UUID()) { do { /// This code, using the `init(entity:insertInto:)` instead of the `init(context:)` avoids warnings during unit testing that look like this: diff --git a/LoopTests/Managers/Alerts/AlertStoreTests.swift b/LoopTests/Managers/Alerts/AlertStoreTests.swift index 3f6286cf17..bb9d109633 100644 --- a/LoopTests/Managers/Alerts/AlertStoreTests.swift +++ b/LoopTests/Managers/Alerts/AlertStoreTests.swift @@ -72,8 +72,8 @@ class AlertStoreTests: XCTestCase { let object = StoredAlert(from: alert2, context: alertStore.managedObjectContext, issuedDate: Self.historicDate) XCTAssertNil(object.acknowledgedDate) XCTAssertNil(object.retractedDate) - XCTAssertEqual("{\"title\":\"title\",\"acknowledgeActionButtonLabel\":\"label\",\"body\":\"body\"}", object.backgroundContent) - XCTAssertEqual("{\"title\":\"title\",\"acknowledgeActionButtonLabel\":\"label\",\"body\":\"body\"}", object.foregroundContent) + XCTAssertEqual("{\"acknowledgeActionButtonLabel\":\"label\",\"body\":\"body\",\"title\":\"title\"}", object.backgroundContent) + XCTAssertEqual("{\"acknowledgeActionButtonLabel\":\"label\",\"body\":\"body\",\"title\":\"title\"}", object.foregroundContent) XCTAssertEqual("managerIdentifier2.alertIdentifier2", object.identifier.value) XCTAssertEqual(Self.historicDate, object.issuedDate) XCTAssertEqual(1, object.modificationCounter) @@ -870,14 +870,14 @@ class AlertStoreLogCriticalEventLogTests: XCTestCase { endDate: dateFormatter.date(from: "2100-01-02T03:09:00Z")!, to: outputStream, progress: progress)) - XCTAssertEqual(outputStream.string, """ -[ -{"acknowledgedDate":"2100-01-02T03:08:00.000Z","alertIdentifier":"a1","backgroundContent":"{\\\"title\\\":\\\"BACKGROUND\\\",\\\"acknowledgeActionButtonLabel\\\":\\\"OK\\\",\\\"body\\\":\\\"background\\\"}","interruptionLevel":"timeSensitive","issuedDate":"2100-01-02T03:08:00.000Z","managerIdentifier":"m1","modificationCounter":1,"syncIdentifier":"52A046F7-F449-49B2-B003-7A378D0002DE","triggerType":0}, -{"acknowledgedDate":"2100-01-02T03:04:00.000Z","alertIdentifier":"a3","backgroundContent":"{\\\"title\\\":\\\"BACKGROUND\\\",\\\"acknowledgeActionButtonLabel\\\":\\\"OK\\\",\\\"body\\\":\\\"background\\\"}","interruptionLevel":"timeSensitive","issuedDate":"2100-01-02T03:04:00.000Z","managerIdentifier":"m3","modificationCounter":3,"syncIdentifier":"285AEA4B-0DEE-41F4-8669-800E9582A6E7","triggerType":0}, -{"acknowledgedDate":"2100-01-02T03:06:00.000Z","alertIdentifier":"a4","backgroundContent":"{\\\"title\\\":\\\"BACKGROUND\\\",\\\"acknowledgeActionButtonLabel\\\":\\\"OK\\\",\\\"body\\\":\\\"background\\\"}","interruptionLevel":"timeSensitive","issuedDate":"2100-01-02T03:06:00.000Z","managerIdentifier":"m4","modificationCounter":4,"syncIdentifier":"4B3109BD-DE11-42BD-A777-D4783459C483","triggerType":0} -] -""" - ) + + XCTAssertEqual(outputStream.string, #""" + [ + {"acknowledgedDate":"2100-01-02T03:08:00.000Z","alertIdentifier":"a1","backgroundContent":"{\"acknowledgeActionButtonLabel\":\"OK\",\"body\":\"background\",\"title\":\"BACKGROUND\"}","interruptionLevel":"timeSensitive","issuedDate":"2100-01-02T03:08:00.000Z","managerIdentifier":"m1","modificationCounter":1,"syncIdentifier":"52A046F7-F449-49B2-B003-7A378D0002DE","triggerType":0}, + {"acknowledgedDate":"2100-01-02T03:04:00.000Z","alertIdentifier":"a3","backgroundContent":"{\"acknowledgeActionButtonLabel\":\"OK\",\"body\":\"background\",\"title\":\"BACKGROUND\"}","interruptionLevel":"timeSensitive","issuedDate":"2100-01-02T03:04:00.000Z","managerIdentifier":"m3","modificationCounter":3,"syncIdentifier":"285AEA4B-0DEE-41F4-8669-800E9582A6E7","triggerType":0}, + {"acknowledgedDate":"2100-01-02T03:06:00.000Z","alertIdentifier":"a4","backgroundContent":"{\"acknowledgeActionButtonLabel\":\"OK\",\"body\":\"background\",\"title\":\"BACKGROUND\"}","interruptionLevel":"timeSensitive","issuedDate":"2100-01-02T03:06:00.000Z","managerIdentifier":"m4","modificationCounter":4,"syncIdentifier":"4B3109BD-DE11-42BD-A777-D4783459C483","triggerType":0} + ] + """#) XCTAssertEqual(progress.completedUnitCount, 3 * 1) } diff --git a/LoopTests/Managers/Alerts/StoredAlertTests.swift b/LoopTests/Managers/Alerts/StoredAlertTests.swift index 504a672fae..85fe753c7d 100644 --- a/LoopTests/Managers/Alerts/StoredAlertTests.swift +++ b/LoopTests/Managers/Alerts/StoredAlertTests.swift @@ -45,10 +45,10 @@ class StoredAlertEncodableTests: XCTestCase { let storedAlert = StoredAlert(from: alert, context: managedObjectContext, syncIdentifier: UUID(uuidString: "A7073F28-0322-4506-A733-CF6E0687BAF7")!) XCTAssertEqual(.active, storedAlert.interruptionLevel) storedAlert.issuedDate = dateFormatter.date(from: "2020-05-14T21:00:12Z")! - try! assertStoredAlertEncodable(storedAlert, encodesJSON: """ + try! assertStoredAlertEncodable(storedAlert, encodesJSON: #""" { "alertIdentifier" : "bar", - "backgroundContent" : "{\\\"title\\\":\\\"BACKGROUND\\\",\\\"acknowledgeActionButtonLabel\\\":\\\"OK\\\",\\\"body\\\":\\\"background\\\"}", + "backgroundContent" : "{\"acknowledgeActionButtonLabel\":\"OK\",\"body\":\"background\",\"title\":\"BACKGROUND\"}", "interruptionLevel" : "active", "issuedDate" : "2020-05-14T21:00:12Z", "managerIdentifier" : "foo", @@ -56,15 +56,15 @@ class StoredAlertEncodableTests: XCTestCase { "syncIdentifier" : "A7073F28-0322-4506-A733-CF6E0687BAF7", "triggerType" : 0 } - """ + """# ) storedAlert.interruptionLevel = .critical XCTAssertEqual(.critical, storedAlert.interruptionLevel) - try! assertStoredAlertEncodable(storedAlert, encodesJSON: """ + try! assertStoredAlertEncodable(storedAlert, encodesJSON: #""" { "alertIdentifier" : "bar", - "backgroundContent" : "{\\\"title\\\":\\\"BACKGROUND\\\",\\\"acknowledgeActionButtonLabel\\\":\\\"OK\\\",\\\"body\\\":\\\"background\\\"}", + "backgroundContent" : "{\"acknowledgeActionButtonLabel\":\"OK\",\"body\":\"background\",\"title\":\"BACKGROUND\"}", "interruptionLevel" : "critical", "issuedDate" : "2020-05-14T21:00:12Z", "managerIdentifier" : "foo", @@ -72,7 +72,7 @@ class StoredAlertEncodableTests: XCTestCase { "syncIdentifier" : "A7073F28-0322-4506-A733-CF6E0687BAF7", "triggerType" : 0 } - """ + """# ) } } diff --git a/LoopTests/ViewModels/BolusEntryViewModelTests.swift b/LoopTests/ViewModels/BolusEntryViewModelTests.swift index c373b639b1..7f2c421ebf 100644 --- a/LoopTests/ViewModels/BolusEntryViewModelTests.swift +++ b/LoopTests/ViewModels/BolusEntryViewModelTests.swift @@ -702,14 +702,14 @@ class BolusEntryViewModelTests: XCTestCase { func testCarbEntryDateAndAbsorptionTimeString() async throws { await setUpViewModel(originalCarbEntry: mockOriginalCarbEntry, potentialCarbEntry: mockPotentialCarbEntry) - XCTAssertEqual("12:00 PM + 0m", bolusEntryViewModel.carbEntryDateAndAbsorptionTimeString) + XCTAssertEqual("12:00 PM + 0m", bolusEntryViewModel.carbEntryDateAndAbsorptionTimeString) } func testCarbEntryDateAndAbsorptionTimeString2() async throws { let potentialCarbEntry = NewCarbEntry(quantity: BolusEntryViewModelTests.exampleCarbQuantity, startDate: Self.exampleStartDate, foodType: nil, absorptionTime: nil) await setUpViewModel(originalCarbEntry: mockOriginalCarbEntry, potentialCarbEntry: potentialCarbEntry) - XCTAssertEqual("12:00 PM", bolusEntryViewModel.carbEntryDateAndAbsorptionTimeString) + XCTAssertEqual("12:00 PM", bolusEntryViewModel.carbEntryDateAndAbsorptionTimeString) } func testIsManualGlucosePromptVisible() throws { From d5065b274d828a9f314dff48996c02cbc28e4866 Mon Sep 17 00:00:00 2001 From: Marion Barker Date: Tue, 23 Apr 2024 19:57:14 -0700 Subject: [PATCH 46/49] make it easier to save a Favorite Food from Carb Entry screen (#2136) --- Loop/View Models/CarbEntryViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Loop/View Models/CarbEntryViewModel.swift b/Loop/View Models/CarbEntryViewModel.swift index 261c529ed5..37dedee326 100644 --- a/Loop/View Models/CarbEntryViewModel.swift +++ b/Loop/View Models/CarbEntryViewModel.swift @@ -140,7 +140,7 @@ final class CarbEntryViewModel: ObservableObject { var saveFavoriteFoodButtonDisabled: Bool { get { - if let carbsQuantity, 0...maxCarbEntryQuantity.doubleValue(for: preferredCarbUnit) ~= carbsQuantity, foodType != "", selectedFavoriteFoodIndex == -1 { + if let carbsQuantity, 0...maxCarbEntryQuantity.doubleValue(for: preferredCarbUnit) ~= carbsQuantity, selectedFavoriteFoodIndex == -1 { return false } return true From 6cf0285e13f6523339b6a4758558cd66d0153c2e Mon Sep 17 00:00:00 2001 From: Marion Barker Date: Tue, 23 Apr 2024 19:58:32 -0700 Subject: [PATCH 47/49] remove shaking phone as trigger for presentDebugMenu (#2149) --- Loop/View Controllers/StatusTableViewController.swift | 9 --------- 1 file changed, 9 deletions(-) diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index 8906a75986..6a4aadfcdd 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -1865,15 +1865,6 @@ final class StatusTableViewController: LoopChartsTableViewController { lastOrientation = UIDevice.current.orientation } - override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) { - guard FeatureFlags.allowDebugFeatures else { - return - } - if motion == .motionShake { - presentDebugMenu() - } - } - private func presentDebugMenu() { guard FeatureFlags.allowDebugFeatures else { return From 1aaee2db4953dbcdbe71a15cad967dbb190dc238 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Sat, 13 Jul 2024 14:43:49 -0500 Subject: [PATCH 48/49] Bump version to 3.5.0 to signify dev branch --- Version.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Version.xcconfig b/Version.xcconfig index 373efdca05..a7c7fe29d1 100644 --- a/Version.xcconfig +++ b/Version.xcconfig @@ -7,7 +7,7 @@ // // Version [DEFAULT] -LOOP_MARKETING_VERSION = 3.3.0 +LOOP_MARKETING_VERSION = 3.5.0 CURRENT_PROJECT_VERSION = 57 // Optional override From 975b4d56202109c50fe39cc1c5c917c148433758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Sch=C3=B6mer?= Date: Mon, 4 Sep 2023 12:41:42 +0200 Subject: [PATCH 49/49] make favorite food translatable --- Loop/Extensions/EditMode.swift | 2 +- Loop/Views/AddEditFavoriteFoodView.swift | 30 ++++++++++-- Loop/Views/FavoriteFoodDetailView.swift | 8 ++-- Loop/Views/SettingsView.swift | 28 ++++++------ Loop/de.lproj/Localizable.strings | 58 ++++++++++++++++++++++-- 5 files changed, 98 insertions(+), 28 deletions(-) diff --git a/Loop/Extensions/EditMode.swift b/Loop/Extensions/EditMode.swift index b1ff303a43..27c824eb2d 100644 --- a/Loop/Extensions/EditMode.swift +++ b/Loop/Extensions/EditMode.swift @@ -10,7 +10,7 @@ import SwiftUI extension EditMode { var title: String { - self == .active ? "Done" : "Edit" + self == .active ? NSLocalizedString("Done", comment: "") : NSLocalizedString("Edit", comment: "") } mutating func toggle() { diff --git a/Loop/Views/AddEditFavoriteFoodView.swift b/Loop/Views/AddEditFavoriteFoodView.swift index b647523a13..ddd01320b9 100644 --- a/Loop/Views/AddEditFavoriteFoodView.swift +++ b/Loop/Views/AddEditFavoriteFoodView.swift @@ -93,20 +93,40 @@ struct AddEditFavoriteFoodView: View { let foodTypeFocused: Binding = Binding(get: { expandedRow == .foodType }, set: { expandedRow = $0 ? .foodType : nil }) let absorptionTimeFocused: Binding = Binding(get: { expandedRow == .absorptionTime }, set: { expandedRow = $0 ? .absorptionTime : nil }) - TextFieldRow(text: $viewModel.name, isFocused: nameFocused, title: "Name", placeholder: "Apple") + TextFieldRow( + text: $viewModel.name, + isFocused: nameFocused, + title: NSLocalizedString("Name", comment: "Label for name in favorite food entry screen"), + placeholder: NSLocalizedString("Apple", comment: "Placeholder for name in favorite food entry screen") + ) CardSectionDivider() - CarbQuantityRow(quantity: $viewModel.carbsQuantity, isFocused: carbQuantityFocused, title: "Carb Quantity", preferredCarbUnit: viewModel.preferredCarbUnit) + CarbQuantityRow( + quantity: $viewModel.carbsQuantity, + isFocused: carbQuantityFocused, + title: NSLocalizedString("Carb Quantity", comment: "Label for carb quantity in favorite food entry screen"), + preferredCarbUnit: viewModel.preferredCarbUnit + ) CardSectionDivider() - EmojiRow(text: $viewModel.foodType, isFocused: foodTypeFocused, emojiType: .food, title: "Food Type") + EmojiRow( + text: $viewModel.foodType, + isFocused: foodTypeFocused, + emojiType: .food, + title: NSLocalizedString("Food Type", comment: "Label for food type in favorite entry screen") + ) CardSectionDivider() - AbsorptionTimePickerRow(absorptionTime: $viewModel.absorptionTime, isFocused: absorptionTimeFocused, validDurationRange: viewModel.absorptionRimesRange, showHowAbsorptionTimeWorks: $showHowAbsorptionTimeWorks) - .padding(.bottom, 2) + AbsorptionTimePickerRow( + absorptionTime: $viewModel.absorptionTime, + isFocused: absorptionTimeFocused, + validDurationRange: viewModel.absorptionRimesRange, + showHowAbsorptionTimeWorks: $showHowAbsorptionTimeWorks + ) + .padding(.bottom, 2) } .padding(.vertical, 12) .padding(.horizontal) diff --git a/Loop/Views/FavoriteFoodDetailView.swift b/Loop/Views/FavoriteFoodDetailView.swift index 44c7a83150..a0fe7d3eef 100644 --- a/Loop/Views/FavoriteFoodDetailView.swift +++ b/Loop/Views/FavoriteFoodDetailView.swift @@ -35,10 +35,10 @@ public struct FavoriteFoodDetailView: View { Section("Information") { VStack(spacing: 16) { let rows: [(field: String, value: String)] = [ - ("Name", food.name), - ("Carb Quantity", food.carbsString(formatter: carbFormatter)), - ("Food Type", food.foodType), - ("Absorption Time", food.absorptionTimeString(formatter: absorptionTimeFormatter)) + (NSLocalizedString("Name", comment: "Label for name in favorite food entry"), food.name), + (NSLocalizedString("Carb Quantity", comment:"Label for carb quantity in favorite food entry"), food.carbsString(formatter: carbFormatter)), + (NSLocalizedString("Food Type", comment:"Label for food type in favorite food entry"), food.foodType), + (NSLocalizedString("Absorption Time", comment:"Label for absorption time in favorite food entry"), food.absorptionTimeString(formatter: absorptionTimeFormatter)) ] ForEach(rows, id: \.field) { row in HStack { diff --git a/Loop/Views/SettingsView.swift b/Loop/Views/SettingsView.swift index c3ec98b8dd..ed9723d243 100644 --- a/Loop/Views/SettingsView.swift +++ b/Loop/Views/SettingsView.swift @@ -29,31 +29,31 @@ public struct SettingsView: View { var id: String { rawValue } - + case deleteCGMData case deletePumpData } - + enum ActionSheet: String, Identifiable { var id: String { rawValue } - + case cgmPicker case pumpPicker case servicePicker } - + enum Sheet: String, Identifiable { var id: String { rawValue } - + case favoriteFoods case therapySettings } } - + @State private var actionSheet: Destination.ActionSheet? @State private var alert: Destination.Alert? @State private var sheet: Destination.Sheet? @@ -254,7 +254,7 @@ extension SettingsView { } } } - + @ViewBuilder private var alertWarning: some View { if viewModel.alertPermissionsChecker.showWarning || viewModel.alertPermissionsChecker.notificationCenterSettings.scheduledDeliveryEnabled { @@ -369,8 +369,8 @@ extension SettingsView { LargeButton(action: { sheet = .favoriteFoods }, includeArrow: true, imageView: Image("Favorite Foods Icon").renderingMode(.template).foregroundColor(carbTintColor), - label: "Favorite Foods", - descriptiveText: "Simplify Carb Entry") + label: NSLocalizedString("Favorite Foods", comment: "Label for favorite foods in settings view"), + descriptiveText: NSLocalizedString("Simplify Carb Entry", comment: "subheadline of favorite foods in settings view")) } } @@ -493,7 +493,7 @@ extension SettingsView { ) } } - + private func createAppExpirationSection(headerLabel: String, footerLabel: String, expirationLabel: String, updateURL: String, nearExpiration: Bool, expirationMessage: String) -> some View { return Section( header: SectionHeader(label: headerLabel), @@ -557,7 +557,7 @@ fileprivate struct LargeButton: View { let secondaryImageView: SecondaryContent let label: String let descriptiveText: String - + init( action: @escaping () -> Void, includeArrow: Bool = true, @@ -593,15 +593,15 @@ fileprivate struct LargeButton: View { DescriptiveText(label: descriptiveText) } } - + if !(secondaryImageView is EmptyView) || includeArrow { Spacer() } - + if !(secondaryImageView is EmptyView) { secondaryImageView.frame(width: secondaryImageWidth, height: secondaryImageHeight) } - + if includeArrow { // TODO: Ick. I can't use a NavigationLink because we're not Navigating, but this seems worse somehow. Image(systemName: "chevron.right").foregroundColor(.gray).font(.footnote) diff --git a/Loop/de.lproj/Localizable.strings b/Loop/de.lproj/Localizable.strings index dc4c9b5d4b..b44b2ff1bc 100755 --- a/Loop/de.lproj/Localizable.strings +++ b/Loop/de.lproj/Localizable.strings @@ -125,6 +125,9 @@ /* Alert message for a missing pump error */ "A pump must be configured before a bolus can be delivered." = "Eine Pumpe muss konfiguriert werden, bevor ein Bolus abgegeben werden kann."; +/* Label for absorption time in favorite food entry */ +"Absorption Time" = "Resorptionsdauer"; + /* Action to copy the recommended Bolus value to the actual Bolus Field */ "AcceptRecommendedBolus" = "Akzeptiere empfohlenen Bolus"; @@ -143,6 +146,9 @@ /* The string format describing active insulin. (1: localized insulin value description) */ "Active Insulin: %@" = "Aktives Insulin: %@"; +/* No comment provided by engineer. */ +"Add a new favorite food" = "Erstelle einen neuen Favoriten"; + /* Title of the user activity for adding carbs */ "Add Carb Entry" = "KH hinzufügen"; @@ -171,17 +177,24 @@ Notification & Critical Alert Permissions screen title */ "Alert Permissions" = "Benachrichtigungsberechtigungen"; +/* Navigation title for algorithms experiments screen + The title of the Algorithm Experiments section in settings */ +"Algorithm Experiments" = "Algorithmusexperimente"; + +/* Algorithm Experiments description. */ +"Algorithm Experiments are optional modifications to the Loop Algorithm. These modifications are less tested than the standard Loop Algorithm, so please use carefully." = "Algorithmusexperimente sind optionale Modifikationen des Schleifenalgorithmus. Diese Modifikationen sind weniger getestet als der Standard-Loop-Algorithmus. Gehe daher bitte vorsichtig vor!"; + /* The title of the section containing algorithm settings */ "Algorithm Settings" = "Algorithmus-Einstellungen"; -/* Label for when mute alert will end */ -"All alerts muted until" = "Alle Alarme stummgeschaltet bis"; - /* No comment provided by engineer. */ "All Favorites" = "Alle Favoriten"; /* Label for carb quantity entry row on carb entry screen */ -"Amount Consumed" = "KH-Menge gegessen"; +"Amount Consumed" = "Menge gegessen"; + +/* Label for when mute alert will end */ +"All alerts muted until" = "Alle Alarme stummgeschaltet bis"; /* The title of the Amplitude service */ "Amplitude" = "Amplitude"; @@ -210,6 +223,9 @@ /* Settings app profile section */ "App Profile" = "App-Profil"; +/* Placeholder for name in favorite food entry screen */ +"Apple" = "Apfel"; + /* Action sheet confirmation message for pump history deletion */ "Are you sure you want to delete all history entries?" = "Möchtest Du wirklich alle Verlaufseinträge löschen?"; @@ -296,6 +312,10 @@ /* Label for carb entry row on bolus screen */ "Carb Entry" = "KH-Eintrag"; +/* Label for carb quantity entry row on favorite food entry screen + Label for carb quantity in favorite food entry */ +"Carb Quantity" = "KH-Menge"; + /* Details for configuration error when carb ratio schedule is missing */ "Carb Ratio Schedule" = "Zeitplan für das Kohlenhydratverhältnis"; @@ -477,6 +497,9 @@ /* Override error description: duration exceed max (1: max duration in hours). */ "Duration exceeds: %1$.1f hours" = "Dauer überschritten: %1$.1f Stunden"; +/* No comment provided by engineer. */ +"Edit" = "Bearbeiten"; + /* Message to the user to enable bluetooth */ "Enable\nBluetooth" = "Bluetooth einschalten"; @@ -540,6 +563,9 @@ /* No comment provided by engineer. */ "FAVORITE FOODS" = "Favorisiertes Essen"; +/* No comment provided by engineer. */ +"Favorite Foods" = "Favorisiertes Essen"; + /* Title of insulin model preset */ "Fiasp" = "Fiasp"; @@ -549,6 +575,10 @@ /* Secondary text for alerts disabled warning, which appears on the main status screen. */ "Fix now by turning Notifications, Critical Alerts and Time Sensitive Notifications ON." = "Behebe dies jetzt, indem Du Benachrichtigungen, kritische Alarme und zeitkritische Benachrichtigungen einschaltest."; +/* label for food type in favorite entry screen + Label for food type in favorite food entry */ +"Food Type" = "Essensart"; + /* The format string used to describe a finite workout targets duration */ "For %1$@" = "Für %1$@"; @@ -567,6 +597,10 @@ /* The title of the glucose and prediction graph */ "Glucose" = "Blutzucker"; +/* Title for glucose based partial application experiment description + Title of glucose based partial application experiment */ +"Glucose Based Partial Application" = "Glucose Based Partial Application"; + /* The error message when glucose data is too old to be used. (1: glucose data age in minutes) */ "Glucose data is %1$@ old" = "Blutzuckerdaten sind %1$@ alt"; @@ -760,6 +794,10 @@ /* Label for button to mute all alerts */ "Mute All Alerts" = "Alle Alarme stummschalten"; +/* Label for name in favorite food entry + Label for name in favorite food entry screen */ +"Name" = "Name"; + /* Sensor state description for the non-valid state */ "Needs Attention" = "Erfordert Aufmerksamkeit"; @@ -938,15 +976,24 @@ /* The title of the notification action to retry a bolus command */ "Retry" = "Wiederholen"; +/* No comment provided by engineer. */ +"Save" = "Speichern"; + /* Button text to save carbs and/or manual glucose entry and deliver a bolus */ "Save and Deliver" = "Speichern und Bolus abgeben"; +/* No comment provided by engineer. */ +"Save as favorite food" = "Als Favorit speichern"; + /* Button text to save carbs and/or manual glucose entry without a bolus */ "Save without Bolusing" = "Speichern ohne Bolusgabe"; /* Scheduled Delivery status text */ "Scheduled" = "Geplant"; +/* No comment provided by engineer. */ +"Selecting a favorite food in the carb entry screen automatically fills in the carb quantity, food type, and absorption time fields! Tap the add button below to create your first favorite food!" = "Wenn Du auf dem Kohlenhydrat-Eingabebildschirm ein Lieblingslebensmittel auswählst, werden automatisch die Felder Kohlenhydratmenge, Lebensmittelart und Absorptionszeit ausgefüllt! Tippe unten auf die Schaltfläche „Hinzufügen“, um Dein erstes Lieblingsessen zu erstellen!"; + /* The title of the services section in settings */ "Services" = "Dienste"; @@ -965,6 +1012,9 @@ /* Title of simple bolus view when displaying meal entry */ "Simple Meal Calculator" = "Einfacher Mahlzeitenrechner"; +/* subheadline of Favorite Foods */ +"Simplify Carb Entry" = "Vereinfachte KH Eingabe"; + /* Format fragment for a start time */ "since %@" = "seit %@";