diff --git a/Common/Extensions/SampleValue.swift b/Common/Extensions/SampleValue.swift index 39dcb16e9c..dd9c901ecf 100644 --- a/Common/Extensions/SampleValue.swift +++ b/Common/Extensions/SampleValue.swift @@ -7,7 +7,7 @@ import HealthKit import LoopKit - +import LoopAlgorithm extension Collection where Element == SampleValue { /// O(n) diff --git a/Common/Models/StatusExtensionContext.swift b/Common/Models/StatusExtensionContext.swift index bee1f32894..8f5f7634fb 100644 --- a/Common/Models/StatusExtensionContext.swift +++ b/Common/Models/StatusExtensionContext.swift @@ -11,6 +11,7 @@ import Foundation import HealthKit import LoopKit import LoopKitUI +import LoopAlgorithm struct NetBasalContext { diff --git a/Common/Models/WatchContext.swift b/Common/Models/WatchContext.swift index 3ce3adebf1..6d4e7a23a0 100644 --- a/Common/Models/WatchContext.swift +++ b/Common/Models/WatchContext.swift @@ -9,6 +9,7 @@ import Foundation import HealthKit import LoopKit +import LoopAlgorithm final class WatchContext: RawRepresentable { diff --git a/Common/Models/WatchHistoricalGlucose.swift b/Common/Models/WatchHistoricalGlucose.swift index 13fda34816..3b166170a9 100644 --- a/Common/Models/WatchHistoricalGlucose.swift +++ b/Common/Models/WatchHistoricalGlucose.swift @@ -9,6 +9,7 @@ import Foundation import HealthKit import LoopKit +import LoopAlgorithm struct WatchHistoricalGlucose { let samples: [StoredGlucoseSample] diff --git a/Common/Models/WatchPredictedGlucose.swift b/Common/Models/WatchPredictedGlucose.swift index 080a824074..8b32a45f01 100644 --- a/Common/Models/WatchPredictedGlucose.swift +++ b/Common/Models/WatchPredictedGlucose.swift @@ -9,6 +9,7 @@ import Foundation import LoopKit import HealthKit +import LoopAlgorithm struct WatchPredictedGlucose: Equatable { diff --git a/Learn/Managers/DataManager.swift b/Learn/Managers/DataManager.swift index 80e958a02f..3929c42bac 100644 --- a/Learn/Managers/DataManager.swift +++ b/Learn/Managers/DataManager.swift @@ -47,7 +47,6 @@ final class DataManager { healthStore: healthStore, cacheStore: cacheStore, observationEnabled: false, - insulinModelProvider: PresetInsulinModelProvider(defaultRapidActingModel: defaultRapidActingModel), longestEffectDuration: ExponentialInsulinModelPreset.rapidActingAdult.effectDuration, basalProfile: basalRateSchedule, insulinSensitivitySchedule: insulinSensitivitySchedule, diff --git a/Loop Status Extension/StatusViewController.swift b/Loop Status Extension/StatusViewController.swift index 16b9b64f10..21a4ada94b 100644 --- a/Loop Status Extension/StatusViewController.swift +++ b/Loop Status Extension/StatusViewController.swift @@ -15,6 +15,7 @@ import LoopUI import NotificationCenter import UIKit import SwiftCharts +import LoopAlgorithm class StatusViewController: UIViewController, NCWidgetProviding { @@ -91,7 +92,6 @@ class StatusViewController: UIViewController, NCWidgetProviding { lazy var doseStore = DoseStore( cacheStore: cacheStore, - insulinModelProvider: PresetInsulinModelProvider(defaultRapidActingModel: settingsStore.latestSettings?.defaultRapidActingModel?.presetForRapidActingInsulin), longestEffectDuration: ExponentialInsulinModelPreset.rapidActingAdult.effectDuration, basalProfile: settingsStore.latestSettings?.basalRateSchedule, insulinSensitivitySchedule: settingsStore.latestSettings?.insulinSensitivitySchedule, @@ -187,17 +187,6 @@ class StatusViewController: UIViewController, NCWidgetProviding { var activeInsulin: Double? let carbUnit = HKUnit.gram() var glucose: [StoredGlucoseSample] = [] - - group.enter() - doseStore.insulinOnBoard(at: Date()) { (result) in - switch result { - case .success(let iobValue): - activeInsulin = iobValue.value - case .failure: - activeInsulin = nil - } - group.leave() - } charts.startDate = Calendar.current.nextDate(after: Date(timeIntervalSinceNow: .minutes(-5)), matching: DateComponents(minute: 0), matchingPolicy: .strict, direction: .backward) ?? Date() diff --git a/Loop Widget Extension/Timeline/StatusWidgetTimelimeEntry.swift b/Loop Widget Extension/Timeline/StatusWidgetTimelimeEntry.swift index d236427e7b..85c22c5649 100644 --- a/Loop Widget Extension/Timeline/StatusWidgetTimelimeEntry.swift +++ b/Loop Widget Extension/Timeline/StatusWidgetTimelimeEntry.swift @@ -10,6 +10,8 @@ import HealthKit import LoopCore import LoopKit import WidgetKit +import LoopAlgorithm + struct StatusWidgetTimelimeEntry: TimelineEntry { var date: Date diff --git a/Loop Widget Extension/Timeline/StatusWidgetTimelineProvider.swift b/Loop Widget Extension/Timeline/StatusWidgetTimelineProvider.swift index 5dd3af7d29..f96f0dde62 100644 --- a/Loop Widget Extension/Timeline/StatusWidgetTimelineProvider.swift +++ b/Loop Widget Extension/Timeline/StatusWidgetTimelineProvider.swift @@ -11,6 +11,7 @@ import LoopCore import LoopKit import OSLog import WidgetKit +import LoopAlgorithm class StatusWidgetTimelineProvider: TimelineProvider { lazy var defaults = UserDefaults.appGroup diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 32667d60ba..69cfe11b5b 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -412,6 +412,7 @@ C1201E2C23ECDBD0002DA84A /* WatchContextRequestUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1201E2B23ECDBD0002DA84A /* WatchContextRequestUserInfo.swift */; }; C1201E2D23ECDF3D002DA84A /* WatchContextRequestUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1201E2B23ECDBD0002DA84A /* WatchContextRequestUserInfo.swift */; }; C129BF4A2B2791EE00DF15CB /* TemporaryPresetsManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C129BF492B2791EE00DF15CB /* TemporaryPresetsManagerTests.swift */; }; + C129D3BF2B8697F100FEA6A9 /* TempBasalRecommendationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C129D3BE2B8697F100FEA6A9 /* TempBasalRecommendationTests.swift */; }; C13072BA2A76AF31009A7C58 /* live_capture_predicted_glucose.json in Resources */ = {isa = PBXBuildFile; fileRef = C13072B92A76AF31009A7C58 /* live_capture_predicted_glucose.json */; }; C13255D6223E7BE2008AF50C /* BolusProgressTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = C1F8B1DB223862D500DD66CF /* BolusProgressTableViewCell.xib */; }; C13DA2B024F6C7690098BB29 /* UIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13DA2AF24F6C7690098BB29 /* UIViewController.swift */; }; @@ -430,6 +431,8 @@ 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 */; }; + C16F51192B891DB600EFD7A1 /* StoredDataAlgorithmInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = C16F51182B891DB600EFD7A1 /* StoredDataAlgorithmInput.swift */; }; + C16F511B2B89363A00EFD7A1 /* SimpleInsulinDose.swift in Sources */ = {isa = PBXBuildFile; fileRef = C16F511A2B89363A00EFD7A1 /* SimpleInsulinDose.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 */; }; @@ -474,7 +477,6 @@ 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 */; }; C1DA434F2B164C6C00CBD33F /* MockSettingsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1DA434E2B164C6C00CBD33F /* MockSettingsProvider.swift */; }; C1DA43532B19310A00CBD33F /* LoopControlMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1DA43522B19310A00CBD33F /* LoopControlMock.swift */; }; @@ -490,6 +492,8 @@ C1F00C60285A802A006302C5 /* SwiftCharts in Frameworks */ = {isa = PBXBuildFile; productRef = C1F00C5F285A802A006302C5 /* SwiftCharts */; }; C1F00C78285A8256006302C5 /* SwiftCharts in Embed Frameworks */ = {isa = PBXBuildFile; productRef = C1F00C5F285A802A006302C5 /* SwiftCharts */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; C1F2075C26D6F9B0007AB7EB /* AppExpirationAlerter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F2075B26D6F9B0007AB7EB /* AppExpirationAlerter.swift */; }; + C1F2CAAA2B76B3EE00D7F581 /* TempBasalRecommendation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F2CAA92B76B3EE00D7F581 /* TempBasalRecommendation.swift */; }; + C1F2CAAC2B7A980600D7F581 /* BasalRelativeDose.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F2CAAB2B7A980600D7F581 /* BasalRelativeDose.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 */; }; @@ -1391,6 +1395,7 @@ C122DEFF29BBFAAE00321F8D /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = ""; }; C122DF0029BBFAAE00321F8D /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = ""; }; C129BF492B2791EE00DF15CB /* TemporaryPresetsManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemporaryPresetsManagerTests.swift; sourceTree = ""; }; + C129D3BE2B8697F100FEA6A9 /* TempBasalRecommendationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempBasalRecommendationTests.swift; sourceTree = ""; }; C12BCCF929BBFA480066A158 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; C12CB9AC23106A3C00F84978 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Intents.strings; sourceTree = ""; }; C12CB9AE23106A5C00F84978 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Intents.strings; sourceTree = ""; }; @@ -1422,6 +1427,8 @@ 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 = ""; }; + C16F51182B891DB600EFD7A1 /* StoredDataAlgorithmInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredDataAlgorithmInput.swift; sourceTree = ""; }; + C16F511A2B89363A00EFD7A1 /* SimpleInsulinDose.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleInsulinDose.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 = ""; }; @@ -1527,7 +1534,6 @@ 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 = ""; }; - 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 = ""; }; C1DA434E2B164C6C00CBD33F /* MockSettingsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSettingsProvider.swift; sourceTree = ""; }; C1DA43522B19310A00CBD33F /* LoopControlMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopControlMock.swift; sourceTree = ""; }; @@ -1557,6 +1563,8 @@ 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 /* AppExpirationAlerter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppExpirationAlerter.swift; sourceTree = ""; }; + C1F2CAA92B76B3EE00D7F581 /* TempBasalRecommendation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempBasalRecommendation.swift; sourceTree = ""; }; + C1F2CAAB2B7A980600D7F581 /* BasalRelativeDose.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasalRelativeDose.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 = ""; }; @@ -1820,7 +1828,6 @@ A91E4C2224F86F1000BE9213 /* CriticalEventLogExportManagerTests.swift */, C188599A2AF15E1B0010F21F /* DeviceDataManagerTests.swift */, C16B983F26B4898800256B05 /* DoseEnactorTests.swift */, - C1D476B32A8ED179002C1C87 /* LoopAlgorithmTests.swift */, E9C58A7124DB489100487A17 /* LoopDataManagerTests.swift */, E9B3552C293592B40076AB04 /* MealDetectionManagerTests.swift */, C1DA43562B1A70BE00CBD33F /* SettingsManagerTests.swift */, @@ -1912,11 +1919,12 @@ C17824A41E1AD4D100D9D25C /* ManualBolusRecommendation.swift */, 4F526D601DF8D9A900A04910 /* NetBasal.swift */, 438D42F81D7C88BC003244B0 /* PredictionInputEffect.swift */, - A99A114029A581D6007919CE /* Remote */, C19008FD25225D3900721625 /* SimpleBolusCalculator.swift */, C1E3862428247B7100F561A4 /* StoredLoopNotRunningNotification.swift */, 4328E0311CFC068900E199AA /* WatchContext+LoopKit.swift */, A987CD4824A58A0100439ADC /* ZipArchive.swift */, + C16F51182B891DB600EFD7A1 /* StoredDataAlgorithmInput.swift */, + C16F511A2B89363A00EFD7A1 /* SimpleInsulinDose.swift */, ); path = Models; sourceTree = ""; @@ -2139,6 +2147,7 @@ children = ( A98556842493F901000FD662 /* AlertStore+SimulatedCoreData.swift */, C1D289B422F90A52003FFBD9 /* BasalDeliveryState.swift */, + C1F2CAAB2B7A980600D7F581 /* BasalRelativeDose.swift */, A9F703722489BC8500C98AD8 /* CarbStore+SimulatedCoreData.swift */, C17824991E1999FA00D9D25C /* CaseCountable.swift */, 4F6663931E905FD2009E74FC /* ChartColorPalette+Loop.swift */, @@ -2165,6 +2174,7 @@ 892A5D682230C41D008961AB /* RangeReplaceableCollection.swift */, A9CBE45B248ACC03008E7BA2 /* SettingsStore+SimulatedCoreData.swift */, C1FB428B217806A300FAB378 /* StateColorPalette.swift */, + C1F2CAA92B76B3EE00D7F581 /* TempBasalRecommendation.swift */, 43F89CA222BDFBBC006BB54E /* UIActivityIndicatorView.swift */, 43F41C361D3BF32400C11ED6 /* UIAlertController.swift */, A9F66FC2247F451500096EA7 /* UIDevice+Loop.swift */, @@ -2668,13 +2678,6 @@ path = Shortcuts; sourceTree = ""; }; - A99A114029A581D6007919CE /* Remote */ = { - isa = PBXGroup; - children = ( - ); - path = Remote; - sourceTree = ""; - }; A9E6DFED246A0460005B1A1C /* Models */ = { isa = PBXGroup; children = ( @@ -2684,6 +2687,7 @@ C19008FF252271BB00721625 /* SimpleBolusCalculatorTests.swift */, A9C1719625366F780053BCBD /* WatchHistoricalGlucoseTest.swift */, A9BD28E6272226B40071DF15 /* TestLocalizedError.swift */, + C129D3BE2B8697F100FEA6A9 /* TempBasalRecommendationTests.swift */, ); path = Models; sourceTree = ""; @@ -3545,6 +3549,7 @@ C16575712538A36B004AE16E /* CGMStalenessMonitor.swift in Sources */, 1D080CBD2473214A00356610 /* AlertStore.xcdatamodeld in Sources */, C11BD0552523CFED00236B08 /* SimpleBolusViewModel.swift in Sources */, + C1F2CAAA2B76B3EE00D7F581 /* TempBasalRecommendation.swift in Sources */, 149A28BB2A853E5100052EDF /* CarbEntryViewModel.swift in Sources */, C19008FE25225D3900721625 /* SimpleBolusCalculator.swift in Sources */, C1F8B243223E73FD00DD66CF /* BolusProgressTableViewCell.swift in Sources */, @@ -3562,6 +3567,7 @@ 142CB75B2A60BFC30075748A /* FavoriteFoodsView.swift in Sources */, A9D5C5B625DC6C6A00534873 /* LoopAppManager.swift in Sources */, 4302F4E11D4E9C8900F0FCAF /* TextFieldTableViewController.swift in Sources */, + C16F51192B891DB600EFD7A1 /* StoredDataAlgorithmInput.swift in Sources */, 1452F4AB2A851EDF00F8B9E4 /* AddEditFavoriteFoodView.swift in Sources */, C1742332259BEADC00399C9D /* ManualEntryDoseView.swift in Sources */, 43F64DD91D9C92C900D24DC6 /* TitleSubtitleTableViewCell.swift in Sources */, @@ -3605,6 +3611,7 @@ E9C00EF524C623EF00628F35 /* LoopSettings+Loop.swift in Sources */, 4389916B1E91B689000EEF90 /* ChartSettings+Loop.swift in Sources */, C178249A1E1999FA00D9D25C /* CaseCountable.swift in Sources */, + C1F2CAAC2B7A980600D7F581 /* BasalRelativeDose.swift in Sources */, B4F3D25124AF890C0095CE44 /* BluetoothStateManager.swift in Sources */, 1DDE273D24AEA4B000796622 /* SettingsViewModel.swift in Sources */, DD3DBD292A33AFE9000F8B5B /* IntegralRetrospectiveCorrectionSelectionView.swift in Sources */, @@ -3641,6 +3648,7 @@ B43CF07E29434EC4008A520B /* HowMuteAlertWorkView.swift in Sources */, 84AA81E52A4A3981000B658B /* DeeplinkManager.swift in Sources */, 1D6B1B6726866D89009AC446 /* AlertPermissionsChecker.swift in Sources */, + C16F511B2B89363A00EFD7A1 /* SimpleInsulinDose.swift in Sources */, 4F08DE8F1E7BB871006741EA /* CollectionType+Loop.swift in Sources */, A9F703772489D8AA00C98AD8 /* PersistentDeviceLog+SimulatedCoreData.swift in Sources */, E9B080B1253BDA6300BAD8F8 /* UserDefaults+LoopIntents.swift in Sources */, @@ -3860,7 +3868,6 @@ E93E86A824DDCC4400FF40C8 /* MockDoseStore.swift in Sources */, B4D4534128E5CA7900F1A8D9 /* AlertMuterTests.swift in Sources */, E98A55F124EDD85E0008715D /* MockDosingDecisionStore.swift in Sources */, - C1D476B42A8ED179002C1C87 /* LoopAlgorithmTests.swift in Sources */, A9BD28E7272226B40071DF15 /* TestLocalizedError.swift in Sources */, A9F5F1F5251050EC00E7C8A4 /* ZipArchiveTests.swift in Sources */, E9B3552D293592B40076AB04 /* MealDetectionManagerTests.swift in Sources */, @@ -3883,6 +3890,7 @@ C129BF4A2B2791EE00DF15CB /* TemporaryPresetsManagerTests.swift in Sources */, E93E86B024DDE1BD00FF40C8 /* MockGlucoseStore.swift in Sources */, C18859A82AF292D90010F21F /* MockTrustedTimeChecker.swift in Sources */, + C129D3BF2B8697F100FEA6A9 /* TempBasalRecommendationTests.swift in Sources */, 1DFE9E172447B6270082C280 /* UserNotificationAlertSchedulerTests.swift in Sources */, E9B3552F2935968E0076AB04 /* HKHealthStoreMock.swift in Sources */, B4BC56382518DEA900373647 /* CGMStatusHUDViewModelTests.swift in Sources */, @@ -4879,7 +4887,7 @@ TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; WARNING_CFLAGS = "-Wall"; - WATCHOS_DEPLOYMENT_TARGET = 7.1; + WATCHOS_DEPLOYMENT_TARGET = 8.0; }; name = Debug; }; @@ -4989,7 +4997,7 @@ VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; WARNING_CFLAGS = "-Wall"; - WATCHOS_DEPLOYMENT_TARGET = 7.1; + WATCHOS_DEPLOYMENT_TARGET = 8.0; }; name = Release; }; @@ -5485,7 +5493,7 @@ TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; WARNING_CFLAGS = "-Wall"; - WATCHOS_DEPLOYMENT_TARGET = 7.1; + WATCHOS_DEPLOYMENT_TARGET = 8.0; }; name = Testflight; }; diff --git a/Loop/Extensions/BasalDeliveryState.swift b/Loop/Extensions/BasalDeliveryState.swift index 6b53f06e2b..32cf77c930 100644 --- a/Loop/Extensions/BasalDeliveryState.swift +++ b/Loop/Extensions/BasalDeliveryState.swift @@ -8,6 +8,7 @@ import LoopKit import LoopCore +import LoopAlgorithm extension PumpManagerStatus.BasalDeliveryState { func getNetBasal(basalSchedule: BasalRateSchedule, maximumBasalRatePerHour: Double?) -> NetBasal? { diff --git a/Loop/Extensions/BasalRelativeDose.swift b/Loop/Extensions/BasalRelativeDose.swift new file mode 100644 index 0000000000..d78d5ad967 --- /dev/null +++ b/Loop/Extensions/BasalRelativeDose.swift @@ -0,0 +1,51 @@ +// +// BasalRelativeDose.swift +// Loop +// +// Created by Pete Schwamb on 2/12/24. +// Copyright © 2024 LoopKit Authors. All rights reserved. +// + +import Foundation +import LoopAlgorithm + +public extension Array where Element == BasalRelativeDose { + func trimmed(from start: Date? = nil, to end: Date? = nil) -> [BasalRelativeDose] { + return self.compactMap { (dose) -> BasalRelativeDose? in + if let start, dose.endDate < start { + return nil + } + if let end, dose.startDate > end { + return nil + } + if dose.type == .bolus { + // Do not split boluses + return dose + } + return dose.trimmed(from: start, to: end) + } + } +} + +extension BasalRelativeDose { + public func trimmed(from start: Date? = nil, to end: Date? = nil, syncIdentifier: String? = nil) -> BasalRelativeDose { + + let originalDuration = endDate.timeIntervalSince(startDate) + + let startDate = max(start ?? .distantPast, self.startDate) + let endDate = max(startDate, min(end ?? .distantFuture, self.endDate)) + + var trimmedVolume: Double = volume + + if originalDuration > .ulpOfOne && (startDate > self.startDate || endDate < self.endDate) { + trimmedVolume = volume * (endDate.timeIntervalSince(startDate) / originalDuration) + } + + return BasalRelativeDose( + type: self.type, + startDate: startDate, + endDate: endDate, + volume: trimmedVolume + ) + } +} diff --git a/Loop/Extensions/CollectionType+Loop.swift b/Loop/Extensions/CollectionType+Loop.swift index 1ca70b1ff9..8740bdb453 100644 --- a/Loop/Extensions/CollectionType+Loop.swift +++ b/Loop/Extensions/CollectionType+Loop.swift @@ -8,6 +8,7 @@ import Foundation import LoopKit +import LoopAlgorithm public extension Sequence where Element: TimelineValue { diff --git a/Loop/Extensions/DosingDecisionStore+SimulatedCoreData.swift b/Loop/Extensions/DosingDecisionStore+SimulatedCoreData.swift index 94627cfdd1..ae4b0c05bc 100644 --- a/Loop/Extensions/DosingDecisionStore+SimulatedCoreData.swift +++ b/Loop/Extensions/DosingDecisionStore+SimulatedCoreData.swift @@ -9,6 +9,7 @@ import Foundation import HealthKit import LoopKit +import LoopAlgorithm // MARK: - Simulated Core Data @@ -168,7 +169,7 @@ fileprivate extension StoredDosingDecision { duration: .minutes(30)), bolusUnits: 1.25) let manualBolusRecommendation = ManualBolusRecommendationWithDate(recommendation: ManualBolusRecommendation(amount: 0.2, - notice: .predictedGlucoseBelowTarget(minGlucose: PredictedGlucoseValue(startDate: date.addingTimeInterval(.minutes(30)), + notice: .predictedGlucoseBelowTarget(minGlucose: SimpleGlucoseValue(startDate: date.addingTimeInterval(.minutes(30)), quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 95.0)))), date: date.addingTimeInterval(-.minutes(1))) let manualBolusRequested = 0.5 diff --git a/Loop/Extensions/SettingsStore+SimulatedCoreData.swift b/Loop/Extensions/SettingsStore+SimulatedCoreData.swift index 5fbcd152f6..e633401d0d 100644 --- a/Loop/Extensions/SettingsStore+SimulatedCoreData.swift +++ b/Loop/Extensions/SettingsStore+SimulatedCoreData.swift @@ -9,6 +9,7 @@ import Foundation import HealthKit import LoopKit +import LoopAlgorithm // MARK: - Simulated Core Data diff --git a/Loop/Extensions/TempBasalRecommendation.swift b/Loop/Extensions/TempBasalRecommendation.swift new file mode 100644 index 0000000000..8d60a52687 --- /dev/null +++ b/Loop/Extensions/TempBasalRecommendation.swift @@ -0,0 +1,67 @@ +// +// TempBasalRecommendation.swift +// Loop +// +// Created by Pete Schwamb on 2/9/24. +// Copyright © 2024 LoopKit Authors. All rights reserved. +// + +import Foundation +import LoopKit +import LoopAlgorithm + +extension TempBasalRecommendation { + /// Equates the recommended rate with another rate + /// + /// - Parameter unitsPerHour: The rate to compare + /// - Returns: Whether the rates are equal within Double precision + private func matchesRate(_ unitsPerHour: Double) -> Bool { + return abs(self.unitsPerHour - unitsPerHour) < .ulpOfOne + } + + /// Adjusts a recommendation based on the current state of pump delivery. If the current temp basal matches + /// the recommendation, and enough time is remaining, then recommend no action. If we are running a temp basal + /// and the new rate matches the scheduled rate, then cancel the currently running temp basal. If the current scheduled + /// rate matches the recommended rate, then recommend no action. Otherwise, set a new temp basal of the + /// recommended rate. + /// + /// - Parameters: + /// - date: The date the recommendation would be delivered + /// - neutralBasalRate: The scheduled basal rate at `date` + /// - lastTempBasal: The previously set temp basal + /// - continuationInterval: The duration of time before an ongoing temp basal should be continued with a new command + /// - neutralBasalRateMatchesPump: A flag describing whether `neutralBasalRate` matches the scheduled basal rate of the pump. + /// If `false` and the recommendation matches `neutralBasalRate`, the temp will be recommended + /// at the scheduled basal rate rather than recommending no temp. + /// - Returns: A temp basal recommendation + func adjustForCurrentDelivery( + at date: Date, + neutralBasalRate: Double, + currentTempBasal: DoseEntry?, + continuationInterval: TimeInterval, + neutralBasalRateMatchesPump: Bool + ) -> TempBasalRecommendation? { + // Adjust behavior for the currently active temp basal + if let currentTempBasal, currentTempBasal.type == .tempBasal, currentTempBasal.endDate > date + { + /// If the last temp basal has the same rate, and has more than `continuationInterval` of time remaining, don't set a new temp + if matchesRate(currentTempBasal.unitsPerHour), + currentTempBasal.endDate.timeIntervalSince(date) > continuationInterval { + return nil + } else if matchesRate(neutralBasalRate), neutralBasalRateMatchesPump { + // If our new temp matches the scheduled rate of the pump, cancel the current temp + return .cancel + } + } else if matchesRate(neutralBasalRate), neutralBasalRateMatchesPump { + // If we recommend the in-progress scheduled basal rate of the pump, do nothing + return nil + } + + return self + } + + public static var cancel: TempBasalRecommendation { + return self.init(unitsPerHour: 0, duration: 0) + } +} + diff --git a/Loop/Extensions/UserDefaults+Loop.swift b/Loop/Extensions/UserDefaults+Loop.swift index 4894dcc777..a663c1e8a4 100644 --- a/Loop/Extensions/UserDefaults+Loop.swift +++ b/Loop/Extensions/UserDefaults+Loop.swift @@ -7,6 +7,7 @@ import Foundation import LoopKit +import LoopAlgorithm extension UserDefaults { diff --git a/Loop/Managers/AppExpirationAlerter.swift b/Loop/Managers/AppExpirationAlerter.swift index e7768f92b6..054f50a5a4 100644 --- a/Loop/Managers/AppExpirationAlerter.swift +++ b/Loop/Managers/AppExpirationAlerter.swift @@ -124,9 +124,9 @@ 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 targetEnvironment(simulator) + 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 { diff --git a/Loop/Managers/CGMStalenessMonitor.swift b/Loop/Managers/CGMStalenessMonitor.swift index ad54f9d1eb..60fe0d06b2 100644 --- a/Loop/Managers/CGMStalenessMonitor.swift +++ b/Loop/Managers/CGMStalenessMonitor.swift @@ -9,6 +9,7 @@ import Foundation import LoopKit import LoopCore +import LoopAlgorithm protocol CGMStalenessMonitorDelegate: AnyObject { func getLatestCGMGlucose(since: Date, completion: @escaping (_ result: Swift.Result) -> Void) diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 67746212f3..149d691be9 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -13,6 +13,7 @@ import LoopCore import LoopTestingKit import UserNotifications import Combine +import LoopAlgorithm protocol LoopControl { var lastLoopCompleted: Date? { get } @@ -1397,7 +1398,7 @@ extension DeviceDataManager: DeliveryDelegate { return pumpManager.roundToSupportedBolusVolume(units: units) } - var pumpInsulinType: LoopKit.InsulinType? { + var pumpInsulinType: InsulinType? { return pumpManager?.status.insulinType } @@ -1405,7 +1406,7 @@ extension DeviceDataManager: DeliveryDelegate { return pumpManager?.status.basalDeliveryState?.isSuspended ?? false } - func enact(_ recommendation: LoopKit.AutomaticDoseRecommendation) async throws { + func enact(_ recommendation: AutomaticDoseRecommendation) async throws { guard let pumpManager = pumpManager else { throw LoopError.configurationError(.pumpManager) } diff --git a/Loop/Managers/DoseEnactor.swift b/Loop/Managers/DoseEnactor.swift index fc533d6219..6777802a5d 100644 --- a/Loop/Managers/DoseEnactor.swift +++ b/Loop/Managers/DoseEnactor.swift @@ -8,6 +8,7 @@ import Foundation import LoopKit +import LoopAlgorithm class DoseEnactor { diff --git a/Loop/Managers/LoopAppManager.swift b/Loop/Managers/LoopAppManager.swift index 2b026a384a..80ea2f046e 100644 --- a/Loop/Managers/LoopAppManager.swift +++ b/Loop/Managers/LoopAppManager.swift @@ -15,7 +15,7 @@ import MockKit import HealthKit import WidgetKit import LoopCore - +import LoopAlgorithm #if targetEnvironment(simulator) enum SimulatorError: Error { @@ -228,8 +228,6 @@ class LoopAppManager: NSObject { observationStart: Date().addingTimeInterval(-CarbMath.maximumAbsorptionTimeInterval) ) - let absorptionTimes = LoopCoreConstants.defaultCarbAbsorptionTimes - temporaryPresetsManager = TemporaryPresetsManager(settingsProvider: settingsManager) temporaryPresetsManager.overrideHistory.delegate = self @@ -239,9 +237,7 @@ class LoopAppManager: NSObject { self.carbStore = CarbStore( healthKitSampleStore: carbHealthStore, cacheStore: cacheStore, - cacheLength: localCacheDuration, - defaultAbsorptionTimes: absorptionTimes, - carbAbsorptionModel: FeatureFlags.nonlinearCarbModelEnabled ? .piecewiseLinear : .linear + cacheLength: localCacheDuration ) let insulinHealthStore = HealthKitSampleStore( @@ -251,19 +247,10 @@ class LoopAppManager: NSObject { observationStart: Date().addingTimeInterval(-CarbMath.maximumAbsorptionTimeInterval) ) - let insulinModelProvider: InsulinModelProvider - - if FeatureFlags.adultChildInsulinModelSelectionEnabled { - insulinModelProvider = PresetInsulinModelProvider(defaultRapidActingModel: settingsManager.settings.defaultRapidActingModel?.presetForRapidActingInsulin) - } else { - insulinModelProvider = PresetInsulinModelProvider(defaultRapidActingModel: nil) - } - self.doseStore = DoseStore( healthKitSampleStore: insulinHealthStore, cacheStore: cacheStore, cacheLength: localCacheDuration, - insulinModelProvider: insulinModelProvider, longestEffectDuration: ExponentialInsulinModelPreset.rapidActingAdult.effectDuration, basalProfile: settingsManager.settings.basalRateSchedule, lastPumpEventsReconciliation: nil // PumpManager is nil at this point. Will update this via addPumpEvents below diff --git a/Loop/Managers/LoopDataManager+CarbAbsorption.swift b/Loop/Managers/LoopDataManager+CarbAbsorption.swift index 2d3053f08d..fc29c06e8a 100644 --- a/Loop/Managers/LoopDataManager+CarbAbsorption.swift +++ b/Loop/Managers/LoopDataManager+CarbAbsorption.swift @@ -9,6 +9,7 @@ import Foundation import LoopKit import HealthKit +import LoopAlgorithm struct CarbAbsorptionReview { var carbEntries: [StoredCarbEntry] @@ -33,7 +34,7 @@ extension LoopDataManager { let doses = try await doseStore.getDoses( start: dosesStart, end: end - ) + ).map { $0.simpleDose(with: insulinModel(for: $0.insulinType)) } dosesStart = doses.map { $0.startDate }.min() ?? dosesStart @@ -82,10 +83,7 @@ extension LoopDataManager { // Overlay basal history on basal doses, splitting doses to get amount delivered relative to basal let annotatedDoses = doses.annotated(with: basal) - let insulinModelProvider = PresetInsulinModelProvider(defaultRapidActingModel: nil) - let insulinEffects = annotatedDoses.glucoseEffects( - insulinModelProvider: insulinModelProvider, insulinSensitivityHistory: sensitivity, from: start.addingTimeInterval(-CarbMath.maximumAbsorptionTimeInterval).dateFlooredToTimeInterval(GlucoseMath.defaultDelta), to: nil) diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index 003ef8a668..0715f7e522 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -13,10 +13,12 @@ import LoopKit import LoopKitUI import LoopCore import WidgetKit +import LoopAlgorithm + struct AlgorithmDisplayState { - var input: LoopAlgorithmInput? - var output: LoopAlgorithmOutput? + var input: StoredDataAlgorithmInput? + var output: AlgorithmOutput? var activeInsulin: InsulinValue? { guard let input, let value = output?.activeInsulin else { @@ -32,7 +34,7 @@ struct AlgorithmDisplayState { return CarbValue(startDate: input.predictionStart, value: value) } - var asTuple: (algoInput: LoopAlgorithmInput?, algoOutput: LoopAlgorithmOutput?) { + var asTuple: (algoInput: StoredDataAlgorithmInput?, algoOutput: AlgorithmOutput?) { return (algoInput: input, algoOutput: output) } } @@ -49,6 +51,17 @@ protocol DeliveryDelegate: AnyObject { func roundBolusVolume(units: Double) -> Double } +extension PumpManagerStatus.BasalDeliveryState { + var currentTempBasal: DoseEntry? { + switch self { + case .tempBasal(let dose): + return dose + default: + return nil + } + } +} + protocol DosingManagerDelegate { func didMakeDosingDecision(_ decision: StoredDosingDecision) } @@ -249,7 +262,23 @@ final class LoopDataManager { } } - func fetchData(for baseTime: Date = Date(), disablingPreMeal: Bool = false) async throws -> LoopAlgorithmInput { + func insulinModel(for type: InsulinType?) -> InsulinModel { + switch type { + case .fiasp: + return ExponentialInsulinModelPreset.fiasp + case .lyumjev: + return ExponentialInsulinModelPreset.lyumjev + case .afrezza: + return ExponentialInsulinModelPreset.afrezza + default: + return settings.defaultRapidActingModel?.presetForRapidActingInsulin?.model ?? ExponentialInsulinModelPreset.rapidActingAdult + } + } + + func fetchData( + for baseTime: Date = Date(), + disablingPreMeal: Bool = false + ) async throws -> StoredDataAlgorithmInput { // Need to fetch doses back as far as t - (DIA + DCA) for Dynamic carbs let dosesInputHistory = CarbMath.maximumAbsorptionTimeInterval + InsulinMath.defaultInsulinActivityDuration @@ -367,11 +396,11 @@ final class LoopDataManager { effectiveBolusApplicationFactor = nil } - return LoopAlgorithmInput( - predictionStart: baseTime, + return StoredDataAlgorithmInput( glucoseHistory: glucose, - doses: doses, + doses: doses.map { $0.simpleDose(with: insulinModel(for: $0.insulinType)) }, carbEntries: carbEntries, + predictionStart: baseTime, basal: basalWithOverrides, sensitivity: sensitivityWithOverrides, carbRatio: carbRatioWithOverrides, @@ -380,11 +409,11 @@ final class LoopDataManager { maxBolus: maxBolus, maxBasalRate: maxBasalRate, useIntegralRetrospectiveCorrection: UserDefaults.standard.integralRetrospectiveCorrectionEnabled, + includePositiveVelocityAndRC: true, carbAbsorptionModel: carbAbsorptionModel, - recommendationInsulinType: deliveryDelegate?.pumpInsulinType ?? .novolog, + recommendationInsulinModel: insulinModel(for: deliveryDelegate?.pumpInsulinType ?? .novolog), recommendationType: .manualBolus, - automaticBolusApplicationFactor: effectiveBolusApplicationFactor - ) + automaticBolusApplicationFactor: effectiveBolusApplicationFactor) } func loopingReEnabled() async { @@ -451,7 +480,8 @@ final class LoopDataManager { var input = try await fetchData(for: loopBaseTime) - let startDate = input.predictionStart + // Trim future basal + input.doses = input.doses.trimmed(to: loopBaseTime) let dosingStrategy = settingsProvider.settings.automaticDosingStrategy input.recommendationType = dosingStrategy.recommendationType @@ -460,15 +490,15 @@ final class LoopDataManager { throw LoopError.missingDataError(.glucose) } - guard startDate.timeIntervalSince(latestGlucose.startDate) <= LoopAlgorithm.inputDataRecencyInterval else { + guard loopBaseTime.timeIntervalSince(latestGlucose.startDate) <= LoopAlgorithm.inputDataRecencyInterval else { throw LoopError.glucoseTooOld(date: latestGlucose.startDate) } - guard latestGlucose.startDate.timeIntervalSince(startDate) <= LoopAlgorithm.inputDataRecencyInterval else { + guard latestGlucose.startDate.timeIntervalSince(loopBaseTime) <= LoopAlgorithm.inputDataRecencyInterval else { throw LoopError.invalidFutureGlucose(date: latestGlucose.startDate) } - guard startDate.timeIntervalSince(doseStore.lastAddedPumpData) <= LoopAlgorithm.inputDataRecencyInterval else { + guard loopBaseTime.timeIntervalSince(doseStore.lastAddedPumpData) <= LoopAlgorithm.inputDataRecencyInterval else { throw LoopError.pumpDataTooOld(date: doseStore.lastAddedPumpData) } @@ -491,14 +521,13 @@ final class LoopDataManager { if var basal = algoRecommendation.basalAdjustment { basal.unitsPerHour = deliveryDelegate.roundBasalRate(unitsPerHour: basal.unitsPerHour) - let lastTempBasal = input.doses.first { $0.type == .tempBasal && $0.startDate < input.predictionStart && $0.endDate > input.predictionStart } let scheduledBasalRate = input.basal.closestPrior(to: loopBaseTime)!.value let activeOverride = temporaryPresetsManager.overrideHistory.activeOverride(at: loopBaseTime) - let basalAdjustment = basal.ifNecessary( + let basalAdjustment = basal.adjustForCurrentDelivery( at: loopBaseTime, neutralBasalRate: scheduledBasalRate, - lastTempBasal: lastTempBasal, + currentTempBasal: deliveryDelegate.basalDeliveryState?.currentTempBasal, continuationInterval: .minutes(11), neutralBasalRateMatchesPump: activeOverride == nil ) @@ -555,9 +584,9 @@ final class LoopDataManager { ) async throws -> ManualBolusRecommendation? { var input = try await self.fetchData(for: now(), disablingPreMeal: potentialCarbEntry != nil) - .addingGlucoseSample(sample: manualGlucoseSample) + .addingGlucoseSample(sample: manualGlucoseSample?.asStoredGlucoseStample) .removingCarbEntry(carbEntry: originalCarbEntry) - .addingCarbEntry(carbEntry: potentialCarbEntry) + .addingCarbEntry(carbEntry: potentialCarbEntry?.asStoredCarbEntry) input.includePositiveVelocityAndRC = usePositiveMomentumAndRCForManualBoluses input.recommendationType = .manualBolus @@ -573,10 +602,10 @@ final class LoopDataManager { } var iobValues: [InsulinValue] { - dosesRelativeToBasal.insulinOnBoard() + dosesRelativeToBasal.insulinOnBoardTimeline() } - var dosesRelativeToBasal: [DoseEntry] { + var dosesRelativeToBasal: [BasalRelativeDose] { displayState.output?.dosesRelativeToBasal ?? [] } @@ -714,10 +743,10 @@ extension LoopDataManager { /// Estimate glucose effects of suspending insulin delivery over duration of insulin action starting at the specified date func insulinDeliveryEffect(at date: Date, insulinType: InsulinType) async throws -> [GlucoseEffect] { let startSuspend = date - let insulinEffectDuration = LoopAlgorithm.insulinModelProvider.model(for: insulinType).effectDuration + let insulinEffectDuration = insulinModel(for: insulinType).effectDuration let endSuspend = startSuspend.addingTimeInterval(insulinEffectDuration) - var suspendDoses: [DoseEntry] = [] + var suspendDoses: [BasalRelativeDose] = [] let basal = try await settingsProvider.getBasalHistory(startDate: startSuspend, endDate: endSuspend) let sensitivity = try await settingsProvider.getInsulinSensitivityHistory(startDate: startSuspend, endDate: endSuspend) @@ -743,14 +772,18 @@ extension LoopDataManager { endSuspendDoseDate = basal[index + 1].startDate } - let suspendDose = DoseEntry(type: .tempBasal, startDate: startSuspendDoseDate, endDate: endSuspendDoseDate, value: -basalItem.value, unit: DoseUnit.unitsPerHour) + let suspendDose = BasalRelativeDose( + type: .basal(scheduledRate: basalItem.value), + startDate: startSuspendDoseDate, + endDate: endSuspendDoseDate, + volume: 0 + ) suspendDoses.append(suspendDose) } // Calculate predicted glucose effect of suspending insulin delivery return suspendDoses.glucoseEffects( - insulinModelProvider: LoopAlgorithm.insulinModelProvider, insulinSensitivityHistory: sensitivity ).filterDateRange(startSuspend, endSuspend) } @@ -836,9 +869,9 @@ extension NewGlucoseSample { } -extension LoopAlgorithmInput { +extension StoredDataAlgorithmInput { - func addingDose(dose: DoseEntry?) -> LoopAlgorithmInput { + func addingDose(dose: InsulinDoseType?) -> StoredDataAlgorithmInput { var rval = self if let dose { rval.doses = doses + [dose] @@ -846,23 +879,23 @@ extension LoopAlgorithmInput { return rval } - func addingGlucoseSample(sample: NewGlucoseSample?) -> LoopAlgorithmInput { + func addingGlucoseSample(sample: GlucoseType?) -> StoredDataAlgorithmInput { var rval = self if let sample { - rval.glucoseHistory.append(sample.asStoredGlucoseStample) + rval.glucoseHistory.append(sample) } return rval } - func addingCarbEntry(carbEntry: NewCarbEntry?) -> LoopAlgorithmInput { + func addingCarbEntry(carbEntry: CarbType?) -> StoredDataAlgorithmInput { var rval = self if let carbEntry { - rval.carbEntries = carbEntries + [carbEntry.asStoredCarbEntry] + rval.carbEntries = carbEntries + [carbEntry] } return rval } - func removingCarbEntry(carbEntry: StoredCarbEntry?) -> LoopAlgorithmInput { + func removingCarbEntry(carbEntry: CarbType?) -> StoredDataAlgorithmInput { guard let carbEntry else { return self } @@ -978,7 +1011,7 @@ extension LoopDataManager: ServicesManagerDelegate { func deliverCarbs(amountInGrams: Double, absorptionTime: TimeInterval?, foodType: String?, startDate: Date?) async throws { - let absorptionTime = absorptionTime ?? carbStore.defaultAbsorptionTimes.medium + let absorptionTime = absorptionTime ?? LoopCoreConstants.defaultCarbAbsorptionTimes.medium if absorptionTime < LoopConstants.minCarbAbsorptionTime || absorptionTime > LoopConstants.maxCarbAbsorptionTime { throw CarbActionError.invalidAbsorptionTime(absorptionTime) } @@ -1043,7 +1076,7 @@ extension LoopDataManager: ServicesManagerDelegate { extension LoopDataManager: SimpleBolusViewModelDelegate { - func insulinOnBoard(at date: Date) async -> LoopKit.InsulinValue? { + func insulinOnBoard(at date: Date) async -> InsulinValue? { displayState.activeInsulin } @@ -1083,7 +1116,7 @@ extension LoopDataManager: BolusEntryViewModelDelegate { temporaryPresetsManager.effectiveGlucoseTargetRangeSchedule(presumingMealEntry: presumingMealEntry) } - func generatePrediction(input: LoopAlgorithmInput) throws -> [PredictedGlucoseValue] { + func generatePrediction(input: StoredDataAlgorithmInput) throws -> [PredictedGlucoseValue] { try input.predictGlucose() } } @@ -1094,8 +1127,8 @@ extension LoopDataManager: CarbEntryViewModelDelegate { temporaryPresetsManager.scheduleOverrideEnabled(at: date) } - var defaultAbsorptionTimes: LoopKit.CarbStore.DefaultAbsorptionTimes { - carbStore.defaultAbsorptionTimes + var defaultAbsorptionTimes: DefaultAbsorptionTimes { + LoopCoreConstants.defaultCarbAbsorptionTimes } } @@ -1114,7 +1147,7 @@ extension LoopDataManager: ManualDoseViewModelDelegate { } func insulinActivityDuration(for type: InsulinType?) -> TimeInterval { - return LoopAlgorithm.insulinModelProvider.model(for: type).effectDuration + return insulinModel(for: type).effectDuration } var algorithmDisplayState: AlgorithmDisplayState { @@ -1134,8 +1167,14 @@ extension AutomaticDosingStrategy { } } +extension AutomaticDoseRecommendation { + public var hasDosingChange: Bool { + return basalAdjustment != nil || bolusUnits != nil + } +} + extension StoredDosingDecision { - mutating func updateFrom(input: LoopAlgorithmInput, output: LoopAlgorithmOutput) { + mutating func updateFrom(input: StoredDataAlgorithmInput, output: AlgorithmOutput) { self.historicalGlucose = input.glucoseHistory.map { HistoricalGlucoseValue(startDate: $0.startDate, quantity: $0.quantity) } switch output.recommendationResult { case .success(let recommendation): diff --git a/Loop/Managers/Missed Meal Detection/MealDetectionManager.swift b/Loop/Managers/Missed Meal Detection/MealDetectionManager.swift index bf000d3e95..e014a4332d 100644 --- a/Loop/Managers/Missed Meal Detection/MealDetectionManager.swift +++ b/Loop/Managers/Missed Meal Detection/MealDetectionManager.swift @@ -12,6 +12,7 @@ import OSLog import LoopCore import LoopKit import Combine +import LoopAlgorithm enum MissedMealStatus: Equatable { case hasMissedMeal(startTime: Date, carbAmount: Double) @@ -389,3 +390,27 @@ extension BolusStateProvider { } } +extension GlucoseEffectVelocity { + /// The integration of the velocity span from `start` to `end` + public func effect(from start: Date, to end: Date) -> GlucoseEffect? { + guard + start <= end, + startDate <= start, + end <= endDate + else { + return nil + } + + let duration = end.timeIntervalSince(start) + let velocityPerSecond = quantity.doubleValue(for: GlucoseEffectVelocity.perSecondUnit) + + return GlucoseEffect( + startDate: end, + quantity: HKQuantity( + unit: .milligramsPerDeciliter, + doubleValue: velocityPerSecond * duration + ) + ) + } +} + diff --git a/Loop/Managers/SettingsManager.swift b/Loop/Managers/SettingsManager.swift index cbac8f6b2d..f564e7a7d6 100644 --- a/Loop/Managers/SettingsManager.swift +++ b/Loop/Managers/SettingsManager.swift @@ -15,6 +15,7 @@ import Combine import LoopCore import LoopKitUI import os.log +import LoopAlgorithm protocol DeviceStatusProvider { diff --git a/Loop/Managers/StatusChartsManager.swift b/Loop/Managers/StatusChartsManager.swift index 79ec51ad62..f047a27575 100644 --- a/Loop/Managers/StatusChartsManager.swift +++ b/Loop/Managers/StatusChartsManager.swift @@ -10,6 +10,7 @@ import LoopKit import LoopUI import LoopKitUI import SwiftCharts +import LoopAlgorithm class StatusChartsManager: ChartsManager { @@ -115,7 +116,7 @@ extension StatusChartsManager { extension StatusChartsManager { - func setDoseEntries(_ doseEntries: [DoseEntry]) { + func setDoseEntries(_ doseEntries: [BasalRelativeDose]) { dose.doseEntries = doseEntries invalidateChart(atIndex: ChartIndex.dose.rawValue) } diff --git a/Loop/Managers/Store Protocols/CarbStoreProtocol.swift b/Loop/Managers/Store Protocols/CarbStoreProtocol.swift index bf41a4d3fd..0904631016 100644 --- a/Loop/Managers/Store Protocols/CarbStoreProtocol.swift +++ b/Loop/Managers/Store Protocols/CarbStoreProtocol.swift @@ -19,8 +19,6 @@ protocol CarbStoreProtocol: AnyObject { func deleteCarbEntry(_ oldEntry: StoredCarbEntry) async throws -> Bool - var defaultAbsorptionTimes: CarbStore.DefaultAbsorptionTimes { get } - } extension CarbStore: CarbStoreProtocol { } diff --git a/Loop/Managers/Store Protocols/DoseStoreProtocol.swift b/Loop/Managers/Store Protocols/DoseStoreProtocol.swift index 3bd2bcbdbb..29eb70b7ea 100644 --- a/Loop/Managers/Store Protocols/DoseStoreProtocol.swift +++ b/Loop/Managers/Store Protocols/DoseStoreProtocol.swift @@ -8,6 +8,7 @@ import LoopKit import HealthKit +import LoopAlgorithm protocol DoseStoreProtocol: AnyObject { func getDoses(start: Date?, end: Date?) async throws -> [DoseEntry] diff --git a/Loop/Models/BolusDosingDecision.swift b/Loop/Models/BolusDosingDecision.swift index 9d63905858..4d3002d2ba 100644 --- a/Loop/Models/BolusDosingDecision.swift +++ b/Loop/Models/BolusDosingDecision.swift @@ -7,6 +7,7 @@ // import LoopKit +import LoopAlgorithm struct BolusDosingDecision { enum Reason: String { diff --git a/Loop/Models/ConstantApplicationFactorStrategy.swift b/Loop/Models/ConstantApplicationFactorStrategy.swift index 0ef8dc1d13..82ab6ebad6 100644 --- a/Loop/Models/ConstantApplicationFactorStrategy.swift +++ b/Loop/Models/ConstantApplicationFactorStrategy.swift @@ -10,6 +10,7 @@ import Foundation import HealthKit import LoopKit import LoopCore +import LoopAlgorithm struct ConstantApplicationFactorStrategy: ApplicationFactorStrategy { func calculateDosingFactor( diff --git a/Loop/Models/CrashRecoveryManager.swift b/Loop/Models/CrashRecoveryManager.swift index e0f0e6f260..2e2a249e9c 100644 --- a/Loop/Models/CrashRecoveryManager.swift +++ b/Loop/Models/CrashRecoveryManager.swift @@ -8,6 +8,7 @@ import Foundation import LoopKit +import LoopAlgorithm class CrashRecoveryManager { diff --git a/Loop/Models/GlucoseEffectVelocity.swift b/Loop/Models/GlucoseEffectVelocity.swift index 9557f2fd50..6680073769 100644 --- a/Loop/Models/GlucoseEffectVelocity.swift +++ b/Loop/Models/GlucoseEffectVelocity.swift @@ -7,6 +7,7 @@ import HealthKit import LoopKit +import LoopAlgorithm extension GlucoseEffectVelocity: RawRepresentable { diff --git a/Loop/Models/ManualBolusRecommendation.swift b/Loop/Models/ManualBolusRecommendation.swift index d176b77cf8..1753813e2c 100644 --- a/Loop/Models/ManualBolusRecommendation.swift +++ b/Loop/Models/ManualBolusRecommendation.swift @@ -9,6 +9,7 @@ import Foundation import LoopKit import HealthKit +import LoopAlgorithm extension BolusRecommendationNotice { @@ -37,28 +38,3 @@ extension BolusRecommendationNotice { } } -extension BolusRecommendationNotice: Equatable { - public static func ==(lhs: BolusRecommendationNotice, rhs: BolusRecommendationNotice) -> Bool { - switch (lhs, rhs) { - case (.glucoseBelowSuspendThreshold, .glucoseBelowSuspendThreshold): - return true - - case (.currentGlucoseBelowTarget, .currentGlucoseBelowTarget): - return true - - case (let .predictedGlucoseBelowTarget(minGlucose1), let .predictedGlucoseBelowTarget(minGlucose2)): - // GlucoseValue is not equatable - return - minGlucose1.startDate == minGlucose2.startDate && - minGlucose1.endDate == minGlucose2.endDate && - minGlucose1.quantity == minGlucose2.quantity - - case (.predictedGlucoseInRange, .predictedGlucoseInRange): - return true - - default: - return false - } - } -} - diff --git a/Loop/Models/NetBasal.swift b/Loop/Models/NetBasal.swift index ff11e9e064..02a349a602 100644 --- a/Loop/Models/NetBasal.swift +++ b/Loop/Models/NetBasal.swift @@ -8,6 +8,7 @@ import Foundation import LoopKit +import LoopAlgorithm /// Max basal should generally be set, but in those cases where it isn't just use 3.0U/hr as a default top of scale, so we can show *something*. fileprivate let defaultMaxBasalForScale = 3.0 diff --git a/Loop/Models/PredictionInputEffect.swift b/Loop/Models/PredictionInputEffect.swift index 164db3a234..175afd3c1b 100644 --- a/Loop/Models/PredictionInputEffect.swift +++ b/Loop/Models/PredictionInputEffect.swift @@ -9,6 +9,7 @@ import Foundation import HealthKit import LoopKit +import LoopAlgorithm struct PredictionInputEffect: OptionSet { let rawValue: Int diff --git a/Loop/Models/SimpleInsulinDose.swift b/Loop/Models/SimpleInsulinDose.swift new file mode 100644 index 0000000000..6235d69768 --- /dev/null +++ b/Loop/Models/SimpleInsulinDose.swift @@ -0,0 +1,86 @@ +// +// SimpleInsulinDose.swift +// Loop +// +// Created by Pete Schwamb on 2/23/24. +// Copyright © 2024 LoopKit Authors. All rights reserved. +// + +import Foundation +import LoopKit +import LoopAlgorithm + +// Implements the bare minimum of InsulinDose, including a slot for InsulinModel +// We could use DoseEntry, but we need to dynamically lookup user's preferred +// fast acting insulin model in settings. So until that is removed, we need this. +struct SimpleInsulinDose: InsulinDose { + var deliveryType: InsulinDeliveryType + var startDate: Date + var endDate: Date + var volume: Double + var insulinModel: InsulinModel +} + +extension DoseEntry { + public var deliveryType: InsulinDeliveryType { + switch self.type { + case .bolus: + return .bolus + default: + return .basal + } + } + + public var volume: Double { + return deliveredUnits ?? programmedUnits + } + + func simpleDose(with model: InsulinModel) -> SimpleInsulinDose { + SimpleInsulinDose( + deliveryType: deliveryType, + startDate: startDate, + endDate: endDate, + volume: volume, + insulinModel: model + ) + } +} + +extension Array where Element == SimpleInsulinDose { + func trimmed(to end: Date? = nil) -> [SimpleInsulinDose] { + return self.compactMap { (dose) -> SimpleInsulinDose? in + if let end, dose.startDate > end { + return nil + } + if dose.deliveryType == .bolus { + return dose + } + return dose.trimmed(to: end) + } + } +} + +extension SimpleInsulinDose { + public func trimmed(from start: Date? = nil, to end: Date? = nil, syncIdentifier: String? = nil) -> SimpleInsulinDose { + + let originalDuration = endDate.timeIntervalSince(startDate) + + let startDate = max(start ?? .distantPast, self.startDate) + let endDate = max(startDate, min(end ?? .distantFuture, self.endDate)) + + var trimmedVolume: Double = volume + + if originalDuration > .ulpOfOne && (startDate > self.startDate || endDate < self.endDate) { + trimmedVolume = volume * (endDate.timeIntervalSince(startDate) / originalDuration) + } + + return SimpleInsulinDose( + deliveryType: self.deliveryType, + startDate: startDate, + endDate: endDate, + volume: trimmedVolume, + insulinModel: insulinModel + ) + } +} + diff --git a/Loop/Models/StoredDataAlgorithmInput.swift b/Loop/Models/StoredDataAlgorithmInput.swift new file mode 100644 index 0000000000..321614a99c --- /dev/null +++ b/Loop/Models/StoredDataAlgorithmInput.swift @@ -0,0 +1,54 @@ +// +// StoredDataAlgorithmInput.swift +// Loop +// +// Created by Pete Schwamb on 2/23/24. +// Copyright © 2024 LoopKit Authors. All rights reserved. +// + +import Foundation +import LoopKit +import HealthKit +import LoopAlgorithm + +struct StoredDataAlgorithmInput: AlgorithmInput { + typealias CarbType = StoredCarbEntry + + typealias GlucoseType = StoredGlucoseSample + + typealias InsulinDoseType = SimpleInsulinDose + + var glucoseHistory: [StoredGlucoseSample] + + var doses: [SimpleInsulinDose] + + var carbEntries: [StoredCarbEntry] + + var predictionStart: Date + + var basal: [AbsoluteScheduleValue] + + var sensitivity: [AbsoluteScheduleValue] + + var carbRatio: [AbsoluteScheduleValue] + + var target: GlucoseRangeTimeline + + var suspendThreshold: HKQuantity? + + var maxBolus: Double + + var maxBasalRate: Double + + var useIntegralRetrospectiveCorrection: Bool + + var includePositiveVelocityAndRC: Bool + + var carbAbsorptionModel: CarbAbsorptionModel + + var recommendationInsulinModel: InsulinModel + + var recommendationType: DoseRecommendationType + + var automaticBolusApplicationFactor: Double? +} diff --git a/Loop/Models/WatchContext+LoopKit.swift b/Loop/Models/WatchContext+LoopKit.swift index a9adf41da4..c97c316a00 100644 --- a/Loop/Models/WatchContext+LoopKit.swift +++ b/Loop/Models/WatchContext+LoopKit.swift @@ -9,6 +9,7 @@ import Foundation import HealthKit import LoopKit +import LoopAlgorithm extension WatchContext { convenience init(glucose: GlucoseSampleValue?, glucoseUnit: HKUnit?) { diff --git a/Loop/View Controllers/CarbAbsorptionViewController.swift b/Loop/View Controllers/CarbAbsorptionViewController.swift index 378617b680..e17ca700d6 100644 --- a/Loop/View Controllers/CarbAbsorptionViewController.swift +++ b/Loop/View Controllers/CarbAbsorptionViewController.swift @@ -13,6 +13,7 @@ import LoopKit import LoopKitUI import LoopUI import os.log +import LoopAlgorithm private extension RefreshContext { @@ -147,7 +148,7 @@ final class CarbAbsorptionViewController: LoopChartsTableViewController, Identif charts.updateEndDate(chartStartDate.addingTimeInterval(.hours(totalHours+1))) // When there is no data, this allows presenting current hour + 1 let midnight = Calendar.current.startOfDay(for: Date()) - let listStart = min(midnight, chartStartDate, Date(timeIntervalSinceNow: -carbStore.maximumAbsorptionTimeInterval)) + let listStart = min(midnight, chartStartDate, Date(timeIntervalSinceNow: -CarbMath.maximumAbsorptionTimeInterval)) let shouldUpdateGlucose = currentContext.contains(.glucose) let shouldUpdateCarbs = currentContext.contains(.carbs) @@ -344,7 +345,7 @@ final class CarbAbsorptionViewController: LoopChartsTableViewController, Identif } cell.observedProgress = observedProgress - cell.clampedProgress = Float(absorption.clampedProgress.doubleValue(for: .percent())) + cell.clampedProgress = Float(absorption.observedProgress.doubleValue(for: .percent())) cell.observedDateText = absorptionFormatter.string(from: absorption.estimatedDate.duration) // Absorbed time diff --git a/Loop/View Controllers/PredictionTableViewController.swift b/Loop/View Controllers/PredictionTableViewController.swift index 1f48cb0c88..849e7e22d7 100644 --- a/Loop/View Controllers/PredictionTableViewController.swift +++ b/Loop/View Controllers/PredictionTableViewController.swift @@ -13,6 +13,7 @@ import LoopKitUI import LoopUI import UIKit import os.log +import LoopAlgorithm private extension RefreshContext { @@ -125,7 +126,7 @@ class PredictionTableViewController: LoopChartsTableViewController, Identifiable } self.retrospectiveGlucoseDiscrepancies = algoOutput?.effects.retrospectiveGlucoseDiscrepancies - totalRetrospectiveCorrection = algoOutput?.effects.totalGlucoseCorrectionEffect + totalRetrospectiveCorrection = algoOutput?.effects.totalRetrospectiveCorrectionEffect self.glucoseChart.setPredictedGlucoseValues(algoOutput?.predictedGlucose ?? []) diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index 41935ed1f2..4ffe792d4c 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -19,6 +19,7 @@ import SwiftCharts import os.log import Combine import WidgetKit +import LoopAlgorithm private extension RefreshContext { @@ -436,7 +437,7 @@ final class StatusTableViewController: LoopChartsTableViewController { var glucoseSamples: [StoredGlucoseSample]? var predictedGlucoseValues: [GlucoseValue]? var iobValues: [InsulinValue]? - var doseEntries: [DoseEntry]? + var doseEntries: [BasalRelativeDose]? var totalDelivery: Double? var cobValues: [CarbValue]? var carbsOnBoard: HKQuantity? @@ -488,7 +489,7 @@ final class StatusTableViewController: LoopChartsTableViewController { if currentContext.contains(.insulin) { doseEntries = loopManager.dosesRelativeToBasal.trimmed(from: startDate) - iobValues = loopManager.iobValues.trimmed(from: startDate) + iobValues = loopManager.iobValues.filterDateRange(startDate, nil) totalDelivery = try? await loopManager.doseStore.getTotalUnitsDelivered(since: Calendar.current.startOfDay(for: Date())).value } diff --git a/Loop/View Models/BolusEntryViewModel.swift b/Loop/View Models/BolusEntryViewModel.swift index 38532c6495..98d248fbee 100644 --- a/Loop/View Models/BolusEntryViewModel.swift +++ b/Loop/View Models/BolusEntryViewModel.swift @@ -17,25 +17,26 @@ import LoopKitUI import LoopUI import SwiftUI import SwiftCharts +import LoopAlgorithm protocol BolusEntryViewModelDelegate: AnyObject { var settings: StoredSettings { get } var scheduleOverride: TemporaryScheduleOverride? { get } var preMealOverride: TemporaryScheduleOverride? { get } - var pumpInsulinType: InsulinType? { get } var mostRecentGlucoseDataDate: Date? { get } var mostRecentPumpDataDate: Date? { get } - func fetchData(for baseTime: Date, disablingPreMeal: Bool) async throws -> LoopAlgorithmInput + func fetchData(for baseTime: Date, disablingPreMeal: Bool) async throws -> StoredDataAlgorithmInput func effectiveGlucoseTargetRangeSchedule(presumingMealEntry: Bool) -> GlucoseRangeSchedule? - func insulinActivityDuration(for type: InsulinType?) -> TimeInterval func addCarbEntry(_ carbEntry: NewCarbEntry, replacing replacingEntry: StoredCarbEntry?) async throws -> StoredCarbEntry func saveGlucose(sample: NewGlucoseSample) async throws -> StoredGlucoseSample func storeManualBolusDosingDecision(_ bolusDosingDecision: BolusDosingDecision, withDate date: Date) async func enactBolus(units: Double, activationType: BolusActivationType) async throws + func insulinModel(for type: InsulinType?) -> InsulinModel + func recommendManualBolus( manualGlucoseSample: NewGlucoseSample?, potentialCarbEntry: NewCarbEntry?, @@ -43,7 +44,7 @@ protocol BolusEntryViewModelDelegate: AnyObject { ) async throws -> ManualBolusRecommendation? - func generatePrediction(input: LoopAlgorithmInput) throws -> [PredictedGlucoseValue] + func generatePrediction(input: StoredDataAlgorithmInput) throws -> [PredictedGlucoseValue] var activeInsulin: InsulinValue? { get } var activeCarbs: CarbValue? { get } @@ -518,13 +519,14 @@ final class BolusEntryViewModel: ObservableObject { let startDate = now() var input = try await delegate.fetchData(for: startDate, disablingPreMeal: potentialCarbEntry != nil) - let enteredBolusDose = DoseEntry( - type: .bolus, + var insulinModel = delegate.insulinModel(for: deliveryDelegate?.pumpInsulinType) + + let enteredBolusDose = SimpleInsulinDose( + deliveryType: .bolus, startDate: startDate, - value: enteredBolus.doubleValue(for: .internationalUnit()), - unit: .units, - insulinType: deliveryDelegate?.pumpInsulinType, - manuallyEntered: true + endDate: startDate, + volume: enteredBolus.doubleValue(for: .internationalUnit()), + insulinModel: insulinModel ) storedGlucoseValues = input.glucoseHistory @@ -532,9 +534,9 @@ final class BolusEntryViewModel: ObservableObject { // Add potential bolus, carbs, manual glucose input = input .addingDose(dose: enteredBolusDose) - .addingGlucoseSample(sample: manualGlucoseSample) + .addingGlucoseSample(sample: manualGlucoseSample?.asStoredGlucoseStample) .removingCarbEntry(carbEntry: originalCarbEntry) - .addingCarbEntry(carbEntry: potentialCarbEntry) + .addingCarbEntry(carbEntry: potentialCarbEntry?.asStoredCarbEntry) let prediction = try delegate.generatePrediction(input: input) predictedGlucoseValues = prediction @@ -658,7 +660,9 @@ final class BolusEntryViewModel: ObservableObject { let availableWidth = screenWidth - chartManager.fixedHorizontalMargin - 2 * viewMarginInset let totalHours = floor(Double(availableWidth / LoopConstants.minimumChartWidthPerHour)) - let futureHours = ceil((delegate?.insulinActivityDuration(for: delegate?.pumpInsulinType) ?? .hours(4)).hours) + let insulinType = deliveryDelegate?.pumpInsulinType + let insulinModel = delegate?.insulinModel(for: insulinType) + let futureHours = ceil((insulinModel?.effectDuration ?? .hours(4)).hours) let historyHours = max(LoopConstants.statusChartMinimumHistoryDisplay.hours, totalHours - futureHours) let date = Date(timeInterval: -TimeInterval(hours: historyHours), since: now()) diff --git a/Loop/View Models/CarbEntryViewModel.swift b/Loop/View Models/CarbEntryViewModel.swift index ee0cbe12bc..10d47e6000 100644 --- a/Loop/View Models/CarbEntryViewModel.swift +++ b/Loop/View Models/CarbEntryViewModel.swift @@ -10,9 +10,10 @@ import SwiftUI import LoopKit import HealthKit import Combine +import LoopCore protocol CarbEntryViewModelDelegate: AnyObject, BolusEntryViewModelDelegate { - var defaultAbsorptionTimes: CarbStore.DefaultAbsorptionTimes { get } + var defaultAbsorptionTimes: DefaultAbsorptionTimes { get } func scheduleOverrideEnabled(at date: Date) -> Bool } @@ -72,7 +73,7 @@ final class CarbEntryViewModel: ObservableObject { private var absorptionEditIsProgrammatic = false // needed for when absorption time is changed due to favorite food selection, so that absorptionTimeWasEdited does not get set to true @Published var absorptionTime: TimeInterval - let defaultAbsorptionTimes: CarbStore.DefaultAbsorptionTimes + let defaultAbsorptionTimes: DefaultAbsorptionTimes let minAbsorptionTime = LoopConstants.minCarbAbsorptionTime let maxAbsorptionTime = LoopConstants.maxCarbAbsorptionTime var absorptionRimesRange: ClosedRange { diff --git a/Loop/View Models/ManualEntryDoseViewModel.swift b/Loop/View Models/ManualEntryDoseViewModel.swift index 269cd3b735..de960b0e95 100644 --- a/Loop/View Models/ManualEntryDoseViewModel.swift +++ b/Loop/View Models/ManualEntryDoseViewModel.swift @@ -16,6 +16,7 @@ import LoopKit import LoopKitUI import LoopUI import SwiftUI +import LoopAlgorithm enum ManualEntryDoseViewModelError: Error { case notAuthenticated @@ -28,7 +29,7 @@ protocol ManualDoseViewModelDelegate: AnyObject { var scheduleOverride: TemporaryScheduleOverride? { get } func addManuallyEnteredDose(startDate: Date, units: Double, insulinType: InsulinType?) async - func insulinActivityDuration(for type: InsulinType?) -> TimeInterval + func insulinModel(for type: InsulinType?) -> InsulinModel } @MainActor @@ -229,7 +230,15 @@ final class ManualEntryDoseViewModel: ObservableObject { let state = await delegate.algorithmDisplayState - let enteredBolusDose = DoseEntry(type: .bolus, startDate: selectedDoseDate, value: enteredBolus.doubleValue(for: .internationalUnit()), unit: .units, insulinType: selectedInsulinType) + let insulinModel = delegate.insulinModel(for: selectedInsulinType) + + let enteredBolusDose = SimpleInsulinDose( + deliveryType: .bolus, + startDate: selectedDoseDate, + endDate: selectedDoseDate, + volume: enteredBolus.doubleValue(for: .internationalUnit()), + insulinModel: insulinModel + ) self.activeInsulin = state.activeInsulin?.quantity self.activeCarbs = state.activeCarbs?.quantity @@ -277,7 +286,9 @@ final class ManualEntryDoseViewModel: ObservableObject { let availableWidth = screenWidth - chartManager.fixedHorizontalMargin - 2 * viewMarginInset let totalHours = floor(Double(availableWidth / LoopConstants.minimumChartWidthPerHour)) - let futureHours = ceil((delegate?.insulinActivityDuration(for: selectedInsulinType) ?? .hours(4)).hours) + + let insulinModel = delegate?.insulinModel(for: selectedInsulinType) + let futureHours = ceil((insulinModel?.effectDuration.hours ?? .hours(4)).hours) let historyHours = max(LoopConstants.statusChartMinimumHistoryDisplay.hours, totalHours - futureHours) let date = Date(timeInterval: -TimeInterval(hours: historyHours), since: now()) diff --git a/Loop/View Models/SimpleBolusViewModel.swift b/Loop/View Models/SimpleBolusViewModel.swift index 1137f9bd03..3d90042d3e 100644 --- a/Loop/View Models/SimpleBolusViewModel.swift +++ b/Loop/View Models/SimpleBolusViewModel.swift @@ -15,6 +15,7 @@ import SwiftUI import LoopCore import Intents import LocalAuthentication +import LoopAlgorithm protocol SimpleBolusViewModelDelegate: AnyObject { diff --git a/Loop/Views/PredictedGlucoseChartView.swift b/Loop/Views/PredictedGlucoseChartView.swift index b7e34a3bdb..d8a0041fb8 100644 --- a/Loop/Views/PredictedGlucoseChartView.swift +++ b/Loop/Views/PredictedGlucoseChartView.swift @@ -11,6 +11,7 @@ import SwiftUI import LoopKit import LoopKitUI import LoopUI +import LoopAlgorithm struct PredictedGlucoseChartView: UIViewRepresentable { diff --git a/Loop/Views/SimpleBolusView.swift b/Loop/Views/SimpleBolusView.swift index 087cd5a130..6d255f9fb0 100644 --- a/Loop/Views/SimpleBolusView.swift +++ b/Loop/Views/SimpleBolusView.swift @@ -11,6 +11,7 @@ import LoopKit import LoopKitUI import HealthKit import LoopCore +import LoopAlgorithm struct SimpleBolusView: View { @EnvironmentObject private var displayGlucosePreference: DisplayGlucosePreference @@ -380,7 +381,7 @@ struct SimpleBolusCalculatorView_Previews: PreviewProvider { userUpdatedDate: nil) } - func insulinOnBoard(at date: Date) async -> LoopKit.InsulinValue? { + func insulinOnBoard(at date: Date) async -> InsulinValue? { return nil } diff --git a/LoopCore/LoopCoreConstants.swift b/LoopCore/LoopCoreConstants.swift index d33ca167bc..6d8edfea82 100644 --- a/LoopCore/LoopCoreConstants.swift +++ b/LoopCore/LoopCoreConstants.swift @@ -9,11 +9,13 @@ import Foundation import LoopKit +public typealias DefaultAbsorptionTimes = (fast: TimeInterval, medium: TimeInterval, slow: TimeInterval) + public enum LoopCoreConstants { /// The amount of time in the future a glucose value should be considered valid public static let futureGlucoseDataInterval = TimeInterval(minutes: 5) - public static let defaultCarbAbsorptionTimes: CarbStore.DefaultAbsorptionTimes = (fast: .minutes(30), medium: .hours(3), slow: .hours(5)) + public static let defaultCarbAbsorptionTimes: DefaultAbsorptionTimes = (fast: .minutes(30), medium: .hours(3), slow: .hours(5)) /// How much historical glucose to include in a dosing decision /// Somewhat arbitrary, but typical maximum visible in bolus glucose preview diff --git a/LoopCore/LoopSettings.swift b/LoopCore/LoopSettings.swift index 1140f60c99..b93aecf837 100644 --- a/LoopCore/LoopSettings.swift +++ b/LoopCore/LoopSettings.swift @@ -7,6 +7,7 @@ import HealthKit import LoopKit +import LoopAlgorithm public extension AutomaticDosingStrategy { var title: String { diff --git a/LoopCore/NSUserDefaults.swift b/LoopCore/NSUserDefaults.swift index 93fa7e17d6..ed1ebf5a5c 100644 --- a/LoopCore/NSUserDefaults.swift +++ b/LoopCore/NSUserDefaults.swift @@ -9,6 +9,7 @@ import Foundation import LoopKit import HealthKit +import LoopAlgorithm extension UserDefaults { diff --git a/LoopTests/Fixtures/live_capture/live_capture_input.json b/LoopTests/Fixtures/live_capture/live_capture_input.json index 4bd97abaa9..26f8a3593e 100644 --- a/LoopTests/Fixtures/live_capture/live_capture_input.json +++ b/LoopTests/Fixtures/live_capture/live_capture_input.json @@ -2,964 +2,898 @@ "carbEntries" : [ { "absorptionTime" : 10800, - "quantity" : 22, - "startDate" : "2023-06-22T19:20:53Z" + "grams" : 22, + "date" : "2023-06-22T19:20:53Z" }, { "absorptionTime" : 10800, - "quantity" : 75, - "startDate" : "2023-06-22T21:04:45Z" + "grams" : 75, + "date" : "2023-06-22T21:04:45Z" }, { "absorptionTime" : 10800, - "quantity" : 47, - "startDate" : "2023-06-23T02:10:13Z" + "grams" : 47, + "date" : "2023-06-23T02:10:13Z" } ], "doses" : [ - { - "endDate" : "2023-06-22T16:22:40Z", - "startDate" : "2023-06-22T16:12:40Z", - "type" : "basal", - "unit" : "U", - "value" : 0.050000000000000003 - }, - { - "endDate" : "2023-06-22T16:17:54Z", - "startDate" : "2023-06-22T16:17:46Z", - "type" : "bolus", - "unit" : "U", - "value" : 0.20000000000000001 - }, - { - "endDate" : "2023-06-22T16:32:40Z", - "startDate" : "2023-06-22T16:22:40Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0 - }, - { - "endDate" : "2023-06-22T16:47:39Z", - "startDate" : "2023-06-22T16:32:40Z", - "type" : "basal", - "unit" : "U", - "value" : 0.10000000000000001 - }, - { - "endDate" : "2023-06-22T16:57:41Z", - "startDate" : "2023-06-22T16:47:39Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0 - }, - { - "endDate" : "2023-06-22T17:02:38Z", - "startDate" : "2023-06-22T16:57:41Z", - "type" : "basal", - "unit" : "U", - "value" : 0.050000000000000003 - }, - { - "endDate" : "2023-06-22T17:07:38Z", - "startDate" : "2023-06-22T17:02:38Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0.050000000000000003 - }, - { - "endDate" : "2023-06-22T17:22:45Z", - "startDate" : "2023-06-22T17:07:38Z", - "type" : "basal", - "unit" : "U", - "value" : 0.10000000000000001 - }, - { - "endDate" : "2023-06-22T17:12:46Z", - "startDate" : "2023-06-22T17:12:42Z", - "type" : "bolus", - "unit" : "U", - "value" : 0.10000000000000001 - }, - { - "endDate" : "2023-06-22T17:27:39Z", - "startDate" : "2023-06-22T17:22:45Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0 - }, - { - "endDate" : "2023-06-22T17:27:39Z", - "startDate" : "2023-06-22T17:27:39Z", - "type" : "basal", - "unit" : "U", - "value" : 0 - }, - { - "endDate" : "2023-06-22T17:32:39Z", - "startDate" : "2023-06-22T17:27:39Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0.050000000000000003 - }, - { - "endDate" : "2023-06-22T18:07:38Z", - "startDate" : "2023-06-22T17:32:39Z", - "type" : "basal", - "unit" : "U", - "value" : 0.25 - }, - { - "endDate" : "2023-06-22T17:32:45Z", - "startDate" : "2023-06-22T17:32:41Z", - "type" : "bolus", - "unit" : "U", - "value" : 0.10000000000000001 - }, - { - "endDate" : "2023-06-22T17:42:40Z", - "startDate" : "2023-06-22T17:42:38Z", - "type" : "bolus", - "unit" : "U", - "value" : 0.050000000000000003 - }, - { - "endDate" : "2023-06-22T17:47:43Z", - "startDate" : "2023-06-22T17:47:39Z", - "type" : "bolus", - "unit" : "U", - "value" : 0.10000000000000001 - }, - { - "endDate" : "2023-06-22T18:12:38Z", - "startDate" : "2023-06-22T18:07:38Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0 - }, - { - "endDate" : "2023-06-22T19:17:40Z", - "startDate" : "2023-06-22T18:12:38Z", - "type" : "basal", - "unit" : "U", - "value" : 0.45000000000000001 - }, - { - "endDate" : "2023-06-22T19:02:43Z", - "startDate" : "2023-06-22T19:02:39Z", - "type" : "bolus", - "unit" : "U", - "value" : 0.10000000000000001 - }, - { - "endDate" : "2023-06-22T19:22:43Z", - "startDate" : "2023-06-22T19:17:40Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0 - }, - { - "endDate" : "2023-06-22T19:21:49Z", - "startDate" : "2023-06-22T19:21:01Z", - "type" : "bolus", - "unit" : "U", - "value" : 1.2 - }, - { - "endDate" : "2023-06-22T19:37:37Z", - "startDate" : "2023-06-22T19:22:43Z", - "type" : "basal", - "unit" : "U", - "value" : 0.10000000000000001 - }, - { - "endDate" : "2023-06-22T19:27:43Z", - "startDate" : "2023-06-22T19:27:39Z", - "type" : "bolus", - "unit" : "U", - "value" : 0.10000000000000001 - }, - { - "endDate" : "2023-06-22T19:57:48Z", - "startDate" : "2023-06-22T19:37:37Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0 - }, - { - "endDate" : "2023-06-22T19:57:48Z", - "startDate" : "2023-06-22T19:57:48Z", - "type" : "basal", - "unit" : "U", - "value" : 0 - }, - { - "endDate" : "2023-06-22T20:02:39Z", - "startDate" : "2023-06-22T19:57:48Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0 - }, - { - "endDate" : "2023-06-22T20:07:40Z", - "startDate" : "2023-06-22T20:02:39Z", - "type" : "basal", - "unit" : "U", - "value" : 0.050000000000000003 - }, - { - "endDate" : "2023-06-22T20:12:40Z", - "startDate" : "2023-06-22T20:07:40Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0.10000000000000001 - }, - { - "endDate" : "2023-06-22T20:52:45Z", - "startDate" : "2023-06-22T20:12:40Z", - "type" : "basal", - "unit" : "U", - "value" : 0.25 - }, - { - "endDate" : "2023-06-22T21:07:43Z", - "startDate" : "2023-06-22T20:52:45Z", - "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-06-22T21:47:38Z", - "startDate" : "2023-06-22T21:07:43Z", - "type" : "basal", - "unit" : "U", - "value" : 0.25 - }, - { - "endDate" : "2023-06-22T21:12:42Z", - "startDate" : "2023-06-22T21:12:40Z", - "type" : "bolus", - "unit" : "U", - "value" : 0.050000000000000003 - }, - { - "endDate" : "2023-06-22T22:07:39Z", - "startDate" : "2023-06-22T21:47:38Z", - "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-06-22T22:27:46Z", - "startDate" : "2023-06-22T22:27:38Z", - "type" : "bolus", - "unit" : "U", - "value" : 0.20000000000000001 - }, - { - "endDate" : "2023-06-22T22:37:44Z", - "startDate" : "2023-06-22T22:37:40Z", - "type" : "bolus", - "unit" : "U", - "value" : 0.10000000000000001 - }, - { - "endDate" : "2023-06-22T22:42:42Z", - "startDate" : "2023-06-22T22:42:40Z", - "type" : "bolus", - "unit" : "U", - "value" : 0.050000000000000003 - }, - { - "endDate" : "2023-06-22T23:52:44Z", - "startDate" : "2023-06-22T23:42:40Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0 - }, - { - "endDate" : "2023-06-22T23:57:46Z", - "startDate" : "2023-06-22T23:52:44Z", - "type" : "basal", - "unit" : "U", - "value" : 0.050000000000000003 - }, - { - "endDate" : "2023-06-23T00:02:37Z", - "startDate" : "2023-06-22T23:57:46Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0.050000000000000003 - }, - { - "endDate" : "2023-06-23T01:02:52Z", - "startDate" : "2023-06-23T00:02:37Z", - "type" : "basal", - "unit" : "U", - "value" : 0.40000000000000002 - }, - { - "endDate" : "2023-06-23T00:07:42Z", - "startDate" : "2023-06-23T00:07:40Z", - "type" : "bolus", - "unit" : "U", - "value" : 0.050000000000000003 - }, - { - "endDate" : "2023-06-23T00:12:44Z", - "startDate" : "2023-06-23T00:12:38Z", - "type" : "bolus", - "unit" : "U", - "value" : 0.14999999999999999 - }, - { - "endDate" : "2023-06-23T00:22:43Z", - "startDate" : "2023-06-23T00:22:39Z", - "type" : "bolus", - "unit" : "U", - "value" : 0.10000000000000001 - }, - { - "endDate" : "2023-06-23T00:27:49Z", - "startDate" : "2023-06-23T00:27:41Z", - "type" : "bolus", - "unit" : "U", - "value" : 0.20000000000000001 - }, - { - "endDate" : "2023-06-23T00:32:43Z", - "startDate" : "2023-06-23T00:32:39Z", - "type" : "bolus", - "unit" : "U", - "value" : 0.10000000000000001 - }, - { - "endDate" : "2023-06-23T00:37:58Z", - "startDate" : "2023-06-23T00:37:48Z", - "type" : "bolus", - "unit" : "U", - "value" : 0.25 - }, - { - "endDate" : "2023-06-23T00:42:47Z", - "startDate" : "2023-06-23T00:42:39Z", - "type" : "bolus", - "unit" : "U", - "value" : 0.20000000000000001 - }, - { - "endDate" : "2023-06-23T00:47:44Z", - "startDate" : "2023-06-23T00:47:40Z", - "type" : "bolus", - "unit" : "U", - "value" : 0.10000000000000001 - }, - { - "endDate" : "2023-06-23T00:52:51Z", - "startDate" : "2023-06-23T00:52:45Z", - "type" : "bolus", - "unit" : "U", - "value" : 0.14999999999999999 - }, - { - "endDate" : "2023-06-23T01:12:49Z", - "startDate" : "2023-06-23T01:02:52Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0 - }, - { - "endDate" : "2023-06-23T01:17:41Z", - "startDate" : "2023-06-23T01:12:49Z", - "type" : "basal", - "unit" : "U", - "value" : 0.050000000000000003 - }, - { - "endDate" : "2023-06-23T01:12:54Z", - "startDate" : "2023-06-23T01:12:50Z", - "type" : "bolus", - "unit" : "U", - "value" : 0.10000000000000001 - }, - { - "endDate" : "2023-06-23T01:37:39Z", - "startDate" : "2023-06-23T01:17:41Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0 - }, - { - "endDate" : "2023-06-23T01:37:39Z", - "startDate" : "2023-06-23T01:37:39Z", - "type" : "basal", - "unit" : "U", - "value" : 0 - }, - { - "endDate" : "2023-06-23T01:42:38Z", - "startDate" : "2023-06-23T01:37:39Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0 - }, - { - "endDate" : "2023-06-23T02:07:42Z", - "startDate" : "2023-06-23T01:42:38Z", - "type" : "basal", - "unit" : "U", - "value" : 0.14999999999999999 - }, - { - "endDate" : "2023-06-23T01:47:46Z", - "startDate" : "2023-06-23T01:47:38Z", - "type" : "bolus", - "unit" : "U", - "value" : 0.20000000000000001 - }, - { - "endDate" : "2023-06-23T01:52:47Z", - "startDate" : "2023-06-23T01:52:39Z", - "type" : "bolus", - "unit" : "U", - "value" : 0.20000000000000001 - }, - { - "endDate" : "2023-06-23T01:57:50Z", - "startDate" : "2023-06-23T01:57:40Z", - "type" : "bolus", - "unit" : "U", - "value" : 0.25 - }, - { - "endDate" : "2023-06-23T02:02:49Z", - "startDate" : "2023-06-23T02:02:39Z", - "type" : "bolus", - "unit" : "U", - "value" : 0.25 - }, - { - "endDate" : "2023-06-23T02:07:36Z", - "startDate" : "2023-06-23T02:04:30Z", - "type" : "bolus", - "unit" : "U", - "value" : 4.6500000000000004 - }, - { - "endDate" : "2023-06-23T02:27:44Z", - "startDate" : "2023-06-23T02:07:42Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0 - }, - { - "endDate" : "2023-06-23T02:27:44Z", - "startDate" : "2023-06-23T02:27:44Z", - "type" : "basal", - "unit" : "U", - "value" : 0 - }, - { - "endDate" : "2023-06-23T02:47:39Z", - "startDate" : "2023-06-23T02:27:44Z", - "type" : "tempBasal", - "unit" : "U\/hour", - "value" : 0 - } - ], + { + "endDate" : "2023-06-22T16:22:40Z", + "startDate" : "2023-06-22T16:12:40Z", + "type" : "basal", + "volume" : 0.050000000000000003 + }, + { + "endDate" : "2023-06-22T16:17:54Z", + "startDate" : "2023-06-22T16:17:46Z", + "type" : "bolus", + "volume" : 0.20000000000000001 + }, + { + "endDate" : "2023-06-22T16:32:40Z", + "startDate" : "2023-06-22T16:22:40Z", + "type" : "basal", + "volume" : 0 + }, + { + "endDate" : "2023-06-22T16:47:39Z", + "startDate" : "2023-06-22T16:32:40Z", + "type" : "basal", + "volume" : 0.10000000000000001 + }, + { + "endDate" : "2023-06-22T16:57:41Z", + "startDate" : "2023-06-22T16:47:39Z", + "type" : "basal", + "volume" : 0 + }, + { + "endDate" : "2023-06-22T17:02:38Z", + "startDate" : "2023-06-22T16:57:41Z", + "type" : "basal", + "volume" : 0.050000000000000003 + }, + { + "endDate" : "2023-06-22T17:07:38Z", + "startDate" : "2023-06-22T17:02:38Z", + "type" : "basal", + "volume" : 0.0041666666666666666 + }, + { + "endDate" : "2023-06-22T17:22:45Z", + "startDate" : "2023-06-22T17:07:38Z", + "type" : "basal", + "volume" : 0.10000000000000001 + }, + { + "endDate" : "2023-06-22T17:12:46Z", + "startDate" : "2023-06-22T17:12:42Z", + "type" : "bolus", + "volume" : 0.10000000000000001 + }, + { + "endDate" : "2023-06-22T17:27:39Z", + "startDate" : "2023-06-22T17:22:45Z", + "type" : "basal", + "volume" : 0 + }, + { + "endDate" : "2023-06-22T17:27:39Z", + "startDate" : "2023-06-22T17:27:39Z", + "type" : "basal", + "volume" : 0 + }, + { + "endDate" : "2023-06-22T17:32:39Z", + "startDate" : "2023-06-22T17:27:39Z", + "type" : "basal", + "volume" : 0.0041666666666666666 + }, + { + "endDate" : "2023-06-22T18:07:38Z", + "startDate" : "2023-06-22T17:32:39Z", + "type" : "basal", + "volume" : 0.25 + }, + { + "endDate" : "2023-06-22T17:32:45Z", + "startDate" : "2023-06-22T17:32:41Z", + "type" : "bolus", + "volume" : 0.10000000000000001 + }, + { + "endDate" : "2023-06-22T17:42:40Z", + "startDate" : "2023-06-22T17:42:38Z", + "type" : "bolus", + "volume" : 0.050000000000000003 + }, + { + "endDate" : "2023-06-22T17:47:43Z", + "startDate" : "2023-06-22T17:47:39Z", + "type" : "bolus", + "volume" : 0.10000000000000001 + }, + { + "endDate" : "2023-06-22T18:12:38Z", + "startDate" : "2023-06-22T18:07:38Z", + "type" : "basal", + "volume" : 0 + }, + { + "endDate" : "2023-06-22T19:17:40Z", + "startDate" : "2023-06-22T18:12:38Z", + "type" : "basal", + "volume" : 0.45000000000000001 + }, + { + "endDate" : "2023-06-22T19:02:43Z", + "startDate" : "2023-06-22T19:02:39Z", + "type" : "bolus", + "volume" : 0.10000000000000001 + }, + { + "endDate" : "2023-06-22T19:22:43Z", + "startDate" : "2023-06-22T19:17:40Z", + "type" : "basal", + "volume" : 0 + }, + { + "endDate" : "2023-06-22T19:21:49Z", + "startDate" : "2023-06-22T19:21:01Z", + "type" : "bolus", + "volume" : 1.2 + }, + { + "endDate" : "2023-06-22T19:37:37Z", + "startDate" : "2023-06-22T19:22:43Z", + "type" : "basal", + "volume" : 0.10000000000000001 + }, + { + "endDate" : "2023-06-22T19:27:43Z", + "startDate" : "2023-06-22T19:27:39Z", + "type" : "bolus", + "volume" : 0.10000000000000001 + }, + { + "endDate" : "2023-06-22T19:57:48Z", + "startDate" : "2023-06-22T19:37:37Z", + "type" : "basal", + "volume" : 0 + }, + { + "endDate" : "2023-06-22T19:57:48Z", + "startDate" : "2023-06-22T19:57:48Z", + "type" : "basal", + "volume" : 0 + }, + { + "endDate" : "2023-06-22T20:02:39Z", + "startDate" : "2023-06-22T19:57:48Z", + "type" : "basal", + "volume" : 0 + }, + { + "endDate" : "2023-06-22T20:07:40Z", + "startDate" : "2023-06-22T20:02:39Z", + "type" : "basal", + "volume" : 0.050000000000000003 + }, + { + "endDate" : "2023-06-22T20:12:40Z", + "startDate" : "2023-06-22T20:07:40Z", + "type" : "basal", + "volume" : 0.0083333333333333332 + }, + { + "endDate" : "2023-06-22T20:52:45Z", + "startDate" : "2023-06-22T20:12:40Z", + "type" : "basal", + "volume" : 0.25 + }, + { + "endDate" : "2023-06-22T21:07:43Z", + "startDate" : "2023-06-22T20:52:45Z", + "type" : "basal", + "volume" : 0 + }, + { + "endDate" : "2023-06-22T21:07:49Z", + "startDate" : "2023-06-22T21:04:51Z", + "type" : "bolus", + "volume" : 4.4500000000000002 + }, + { + "endDate" : "2023-06-22T21:47:38Z", + "startDate" : "2023-06-22T21:07:43Z", + "type" : "basal", + "volume" : 0.25 + }, + { + "endDate" : "2023-06-22T21:12:42Z", + "startDate" : "2023-06-22T21:12:40Z", + "type" : "bolus", + "volume" : 0.050000000000000003 + }, + { + "endDate" : "2023-06-22T22:07:39Z", + "startDate" : "2023-06-22T21:47:38Z", + "type" : "basal", + "volume" : 0 + }, + { + "endDate" : "2023-06-22T23:42:40Z", + "startDate" : "2023-06-22T22:07:39Z", + "type" : "basal", + "volume" : 0.65000000000000002 + }, + { + "endDate" : "2023-06-22T22:27:46Z", + "startDate" : "2023-06-22T22:27:38Z", + "type" : "bolus", + "volume" : 0.20000000000000001 + }, + { + "endDate" : "2023-06-22T22:37:44Z", + "startDate" : "2023-06-22T22:37:40Z", + "type" : "bolus", + "volume" : 0.10000000000000001 + }, + { + "endDate" : "2023-06-22T22:42:42Z", + "startDate" : "2023-06-22T22:42:40Z", + "type" : "bolus", + "volume" : 0.050000000000000003 + }, + { + "endDate" : "2023-06-22T23:52:44Z", + "startDate" : "2023-06-22T23:42:40Z", + "type" : "basal", + "volume" : 0 + }, + { + "endDate" : "2023-06-22T23:57:46Z", + "startDate" : "2023-06-22T23:52:44Z", + "type" : "basal", + "volume" : 0.050000000000000003 + }, + { + "endDate" : "2023-06-23T00:02:37Z", + "startDate" : "2023-06-22T23:57:46Z", + "type" : "basal", + "volume" : 0.0040416666666666665 + }, + { + "endDate" : "2023-06-23T01:02:52Z", + "startDate" : "2023-06-23T00:02:37Z", + "type" : "basal", + "volume" : 0.40000000000000002 + }, + { + "endDate" : "2023-06-23T00:07:42Z", + "startDate" : "2023-06-23T00:07:40Z", + "type" : "bolus", + "volume" : 0.050000000000000003 + }, + { + "endDate" : "2023-06-23T00:12:44Z", + "startDate" : "2023-06-23T00:12:38Z", + "type" : "bolus", + "volume" : 0.14999999999999999 + }, + { + "endDate" : "2023-06-23T00:22:43Z", + "startDate" : "2023-06-23T00:22:39Z", + "type" : "bolus", + "volume" : 0.10000000000000001 + }, + { + "endDate" : "2023-06-23T00:27:49Z", + "startDate" : "2023-06-23T00:27:41Z", + "type" : "bolus", + "volume" : 0.20000000000000001 + }, + { + "endDate" : "2023-06-23T00:32:43Z", + "startDate" : "2023-06-23T00:32:39Z", + "type" : "bolus", + "volume" : 0.10000000000000001 + }, + { + "endDate" : "2023-06-23T00:37:58Z", + "startDate" : "2023-06-23T00:37:48Z", + "type" : "bolus", + "volume" : 0.25 + }, + { + "endDate" : "2023-06-23T00:42:47Z", + "startDate" : "2023-06-23T00:42:39Z", + "type" : "bolus", + "volume" : 0.20000000000000001 + }, + { + "endDate" : "2023-06-23T00:47:44Z", + "startDate" : "2023-06-23T00:47:40Z", + "type" : "bolus", + "volume" : 0.10000000000000001 + }, + { + "endDate" : "2023-06-23T00:52:51Z", + "startDate" : "2023-06-23T00:52:45Z", + "type" : "bolus", + "volume" : 0.14999999999999999 + }, + { + "endDate" : "2023-06-23T01:12:49Z", + "startDate" : "2023-06-23T01:02:52Z", + "type" : "basal", + "volume" : 0 + }, + { + "endDate" : "2023-06-23T01:17:41Z", + "startDate" : "2023-06-23T01:12:49Z", + "type" : "basal", + "volume" : 0.050000000000000003 + }, + { + "endDate" : "2023-06-23T01:12:54Z", + "startDate" : "2023-06-23T01:12:50Z", + "type" : "bolus", + "volume" : 0.10000000000000001 + }, + { + "endDate" : "2023-06-23T01:37:39Z", + "startDate" : "2023-06-23T01:17:41Z", + "type" : "basal", + "volume" : 0 + }, + { + "endDate" : "2023-06-23T01:37:39Z", + "startDate" : "2023-06-23T01:37:39Z", + "type" : "basal", + "volume" : 0 + }, + { + "endDate" : "2023-06-23T01:42:38Z", + "startDate" : "2023-06-23T01:37:39Z", + "type" : "basal", + "volume" : 0 + }, + { + "endDate" : "2023-06-23T02:07:42Z", + "startDate" : "2023-06-23T01:42:38Z", + "type" : "basal", + "volume" : 0.14999999999999999 + }, + { + "endDate" : "2023-06-23T01:47:46Z", + "startDate" : "2023-06-23T01:47:38Z", + "type" : "bolus", + "volume" : 0.20000000000000001 + }, + { + "endDate" : "2023-06-23T01:52:47Z", + "startDate" : "2023-06-23T01:52:39Z", + "type" : "bolus", + "volume" : 0.20000000000000001 + }, + { + "endDate" : "2023-06-23T01:57:50Z", + "startDate" : "2023-06-23T01:57:40Z", + "type" : "bolus", + "volume" : 0.25 + }, + { + "endDate" : "2023-06-23T02:02:49Z", + "startDate" : "2023-06-23T02:02:39Z", + "type" : "bolus", + "volume" : 0.25 + }, + { + "endDate" : "2023-06-23T02:07:36Z", + "startDate" : "2023-06-23T02:04:30Z", + "type" : "bolus", + "volume" : 4.6500000000000004 + }, + { + "endDate" : "2023-06-23T02:27:44Z", + "startDate" : "2023-06-23T02:07:42Z", + "type" : "basal", + "volume" : 0 + }, + { + "endDate" : "2023-06-23T02:27:44Z", + "startDate" : "2023-06-23T02:27:44Z", + "type" : "basal", + "volume" : 0 + }, + { + "endDate" : "2023-06-23T02:47:39Z", + "startDate" : "2023-06-23T02:27:44Z", + "type" : "basal", + "volume" : 0 + } + ], "glucoseHistory" : [ { - "quantity" : 120, - "startDate" : "2023-06-22T16:42:33Z" + "value" : 120, + "date" : "2023-06-22T16:42:33Z" }, { - "quantity" : 119, - "startDate" : "2023-06-22T16:47:33Z" + "value" : 119, + "date" : "2023-06-22T16:47:33Z" }, { - "quantity" : 120, - "startDate" : "2023-06-22T16:52:34Z" + "value" : 120, + "date" : "2023-06-22T16:52:34Z" }, { - "quantity" : 118, - "startDate" : "2023-06-22T16:57:34Z" + "value" : 118, + "date" : "2023-06-22T16:57:34Z" }, { - "quantity" : 115, - "startDate" : "2023-06-22T17:02:34Z" + "value" : 115, + "date" : "2023-06-22T17:02:34Z" }, { - "quantity" : 120, - "startDate" : "2023-06-22T17:07:34Z" + "value" : 120, + "date" : "2023-06-22T17:07:34Z" }, { - "quantity" : 121, - "startDate" : "2023-06-22T17:12:34Z" + "value" : 121, + "date" : "2023-06-22T17:12:34Z" }, { - "quantity" : 119, - "startDate" : "2023-06-22T17:17:34Z" + "value" : 119, + "date" : "2023-06-22T17:17:34Z" }, { - "quantity" : 116, - "startDate" : "2023-06-22T17:22:34Z" + "value" : 116, + "date" : "2023-06-22T17:22:34Z" }, { - "quantity" : 115, - "startDate" : "2023-06-22T17:27:34Z" + "value" : 115, + "date" : "2023-06-22T17:27:34Z" }, { - "quantity" : 124, - "startDate" : "2023-06-22T17:32:34Z" + "value" : 124, + "date" : "2023-06-22T17:32:34Z" }, { - "quantity" : 114, - "startDate" : "2023-06-22T17:37:34Z" + "value" : 114, + "date" : "2023-06-22T17:37:34Z" }, { - "quantity" : 124, - "startDate" : "2023-06-22T17:42:34Z" + "value" : 124, + "date" : "2023-06-22T17:42:34Z" }, { - "quantity" : 124, - "startDate" : "2023-06-22T17:47:33Z" + "value" : 124, + "date" : "2023-06-22T17:47:33Z" }, { - "quantity" : 124, - "startDate" : "2023-06-22T17:52:34Z" + "value" : 124, + "date" : "2023-06-22T17:52:34Z" }, { - "quantity" : 126, - "startDate" : "2023-06-22T17:57:33Z" + "value" : 126, + "date" : "2023-06-22T17:57:33Z" }, { - "quantity" : 125, - "startDate" : "2023-06-22T18:02:34Z" + "value" : 125, + "date" : "2023-06-22T18:02:34Z" }, { - "quantity" : 118, - "startDate" : "2023-06-22T18:07:34Z" + "value" : 118, + "date" : "2023-06-22T18:07:34Z" }, { - "quantity" : 122, - "startDate" : "2023-06-22T18:12:33Z" + "value" : 122, + "date" : "2023-06-22T18:12:33Z" }, { - "quantity" : 123, - "startDate" : "2023-06-22T18:17:34Z" + "value" : 123, + "date" : "2023-06-22T18:17:34Z" }, { - "quantity" : 123, - "startDate" : "2023-06-22T18:22:34Z" + "value" : 123, + "date" : "2023-06-22T18:22:34Z" }, { - "quantity" : 121, - "startDate" : "2023-06-22T18:27:34Z" + "value" : 121, + "date" : "2023-06-22T18:27:34Z" }, { - "quantity" : 118, - "startDate" : "2023-06-22T18:32:34Z" + "value" : 118, + "date" : "2023-06-22T18:32:34Z" }, { - "quantity" : 116, - "startDate" : "2023-06-22T18:37:34Z" + "value" : 116, + "date" : "2023-06-22T18:37:34Z" }, { - "quantity" : 118, - "startDate" : "2023-06-22T18:42:34Z" + "value" : 118, + "date" : "2023-06-22T18:42:34Z" }, { - "quantity" : 115, - "startDate" : "2023-06-22T18:47:34Z" + "value" : 115, + "date" : "2023-06-22T18:47:34Z" }, { - "quantity" : 117, - "startDate" : "2023-06-22T18:52:34Z" + "value" : 117, + "date" : "2023-06-22T18:52:34Z" }, { - "quantity" : 125, - "startDate" : "2023-06-22T18:57:34Z" + "value" : 125, + "date" : "2023-06-22T18:57:34Z" }, { - "quantity" : 122, - "startDate" : "2023-06-22T19:02:34Z" + "value" : 122, + "date" : "2023-06-22T19:02:34Z" }, { - "quantity" : 119, - "startDate" : "2023-06-22T19:07:34Z" + "value" : 119, + "date" : "2023-06-22T19:07:34Z" }, { - "quantity" : 120, - "startDate" : "2023-06-22T19:12:34Z" + "value" : 120, + "date" : "2023-06-22T19:12:34Z" }, { - "quantity" : 112, - "startDate" : "2023-06-22T19:17:34Z" + "value" : 112, + "date" : "2023-06-22T19:17:34Z" }, { - "quantity" : 111, - "startDate" : "2023-06-22T19:22:34Z" + "value" : 111, + "date" : "2023-06-22T19:22:34Z" }, { - "quantity" : 114, - "startDate" : "2023-06-22T19:27:34Z" + "value" : 114, + "date" : "2023-06-22T19:27:34Z" }, { - "quantity" : 117, - "startDate" : "2023-06-22T19:32:34Z" + "value" : 117, + "date" : "2023-06-22T19:32:34Z" }, { - "quantity" : 107, - "startDate" : "2023-06-22T19:37:34Z" + "value" : 107, + "date" : "2023-06-22T19:37:34Z" }, { - "quantity" : 113, - "startDate" : "2023-06-22T19:42:34Z" + "value" : 113, + "date" : "2023-06-22T19:42:34Z" }, { - "quantity" : 117, - "startDate" : "2023-06-22T19:47:34Z" + "value" : 117, + "date" : "2023-06-22T19:47:34Z" }, { - "quantity" : 109, - "startDate" : "2023-06-22T19:52:34Z" + "value" : 109, + "date" : "2023-06-22T19:52:34Z" }, { - "quantity" : 117, - "startDate" : "2023-06-22T19:57:34Z" + "value" : 117, + "date" : "2023-06-22T19:57:34Z" }, { - "quantity" : 121, - "startDate" : "2023-06-22T20:02:34Z" + "value" : 121, + "date" : "2023-06-22T20:02:34Z" }, { - "quantity" : 121, - "startDate" : "2023-06-22T20:07:34Z" + "value" : 121, + "date" : "2023-06-22T20:07:34Z" }, { - "quantity" : 127, - "startDate" : "2023-06-22T20:12:34Z" + "value" : 127, + "date" : "2023-06-22T20:12:34Z" }, { - "quantity" : 133, - "startDate" : "2023-06-22T20:17:34Z" + "value" : 133, + "date" : "2023-06-22T20:17:34Z" }, { - "quantity" : 131, - "startDate" : "2023-06-22T20:22:34Z" + "value" : 131, + "date" : "2023-06-22T20:22:34Z" }, { - "quantity" : 132, - "startDate" : "2023-06-22T20:27:34Z" + "value" : 132, + "date" : "2023-06-22T20:27:34Z" }, { - "quantity" : 134, - "startDate" : "2023-06-22T20:32:34Z" + "value" : 134, + "date" : "2023-06-22T20:32:34Z" }, { - "quantity" : 134, - "startDate" : "2023-06-22T20:37:34Z" + "value" : 134, + "date" : "2023-06-22T20:37:34Z" }, { - "quantity" : 139, - "startDate" : "2023-06-22T20:42:34Z" + "value" : 139, + "date" : "2023-06-22T20:42:34Z" }, { - "quantity" : 139, - "startDate" : "2023-06-22T20:47:34Z" + "value" : 139, + "date" : "2023-06-22T20:47:34Z" }, { - "quantity" : 132, - "startDate" : "2023-06-22T20:52:34Z" + "value" : 132, + "date" : "2023-06-22T20:52:34Z" }, { - "quantity" : 118, - "startDate" : "2023-06-22T20:57:34Z" + "value" : 118, + "date" : "2023-06-22T20:57:34Z" }, { - "quantity" : 123, - "startDate" : "2023-06-22T21:02:34Z" + "value" : 123, + "date" : "2023-06-22T21:02:34Z" }, { - "quantity" : 122, - "startDate" : "2023-06-22T21:07:34Z" + "value" : 122, + "date" : "2023-06-22T21:07:34Z" }, { - "quantity" : 119, - "startDate" : "2023-06-22T21:12:34Z" + "value" : 119, + "date" : "2023-06-22T21:12:34Z" }, { - "quantity" : 116, - "startDate" : "2023-06-22T21:17:34Z" + "value" : 116, + "date" : "2023-06-22T21:17:34Z" }, { - "quantity" : 113, - "startDate" : "2023-06-22T21:22:34Z" + "value" : 113, + "date" : "2023-06-22T21:22:34Z" }, { - "quantity" : 111, - "startDate" : "2023-06-22T21:27:34Z" + "value" : 111, + "date" : "2023-06-22T21:27:34Z" }, { - "quantity" : 112, - "startDate" : "2023-06-22T21:32:34Z" + "value" : 112, + "date" : "2023-06-22T21:32:34Z" }, { - "quantity" : 107, - "startDate" : "2023-06-22T21:37:34Z" + "value" : 107, + "date" : "2023-06-22T21:37:34Z" }, { - "quantity" : 102, - "startDate" : "2023-06-22T21:42:34Z" + "value" : 102, + "date" : "2023-06-22T21:42:34Z" }, { - "quantity" : 95, - "startDate" : "2023-06-22T21:47:34Z" + "value" : 95, + "date" : "2023-06-22T21:47:34Z" }, { - "quantity" : 96, - "startDate" : "2023-06-22T21:52:34Z" + "value" : 96, + "date" : "2023-06-22T21:52:34Z" }, { - "quantity" : 89, - "startDate" : "2023-06-22T21:57:34Z" + "value" : 89, + "date" : "2023-06-22T21:57:34Z" }, { - "quantity" : 95, - "startDate" : "2023-06-22T22:02:34Z" + "value" : 95, + "date" : "2023-06-22T22:02:34Z" }, { - "quantity" : 95, - "startDate" : "2023-06-22T22:07:34Z" + "value" : 95, + "date" : "2023-06-22T22:07:34Z" }, { - "quantity" : 93, - "startDate" : "2023-06-22T22:12:34Z" + "value" : 93, + "date" : "2023-06-22T22:12:34Z" }, { - "quantity" : 98, - "startDate" : "2023-06-22T22:17:35Z" + "value" : 98, + "date" : "2023-06-22T22:17:35Z" }, { - "quantity" : 95, - "startDate" : "2023-06-22T22:22:35Z" + "value" : 95, + "date" : "2023-06-22T22:22:35Z" }, { - "quantity" : 101, - "startDate" : "2023-06-22T22:27:34Z" + "value" : 101, + "date" : "2023-06-22T22:27:34Z" }, { - "quantity" : 97, - "startDate" : "2023-06-22T22:32:34Z" + "value" : 97, + "date" : "2023-06-22T22:32:34Z" }, { - "quantity" : 108, - "startDate" : "2023-06-22T22:37:35Z" + "value" : 108, + "date" : "2023-06-22T22:37:35Z" }, { - "quantity" : 109, - "startDate" : "2023-06-22T22:42:34Z" + "value" : 109, + "date" : "2023-06-22T22:42:34Z" }, { - "quantity" : 109, - "startDate" : "2023-06-22T22:47:34Z" + "value" : 109, + "date" : "2023-06-22T22:47:34Z" }, { - "quantity" : 114, - "startDate" : "2023-06-22T22:52:34Z" + "value" : 114, + "date" : "2023-06-22T22:52:34Z" }, { - "quantity" : 115, - "startDate" : "2023-06-22T22:57:34Z" + "value" : 115, + "date" : "2023-06-22T22:57:34Z" }, { - "quantity" : 114, - "startDate" : "2023-06-22T23:02:34Z" + "value" : 114, + "date" : "2023-06-22T23:02:34Z" }, { - "quantity" : 121, - "startDate" : "2023-06-22T23:07:34Z" + "value" : 121, + "date" : "2023-06-22T23:07:34Z" }, { - "quantity" : 119, - "startDate" : "2023-06-22T23:12:34Z" + "value" : 119, + "date" : "2023-06-22T23:12:34Z" }, { - "quantity" : 117, - "startDate" : "2023-06-22T23:17:34Z" + "value" : 117, + "date" : "2023-06-22T23:17:34Z" }, { - "quantity" : 120, - "startDate" : "2023-06-22T23:22:35Z" + "value" : 120, + "date" : "2023-06-22T23:22:35Z" }, { - "quantity" : 122, - "startDate" : "2023-06-22T23:27:34Z" + "value" : 122, + "date" : "2023-06-22T23:27:34Z" }, { - "quantity" : 123, - "startDate" : "2023-06-22T23:32:34Z" + "value" : 123, + "date" : "2023-06-22T23:32:34Z" }, { - "quantity" : 127, - "startDate" : "2023-06-22T23:37:34Z" + "value" : 127, + "date" : "2023-06-22T23:37:34Z" }, { - "quantity" : 118, - "startDate" : "2023-06-22T23:42:35Z" + "value" : 118, + "date" : "2023-06-22T23:42:35Z" }, { - "quantity" : 120, - "startDate" : "2023-06-22T23:47:34Z" + "value" : 120, + "date" : "2023-06-22T23:47:34Z" }, { - "quantity" : 119, - "startDate" : "2023-06-22T23:52:35Z" + "value" : 119, + "date" : "2023-06-22T23:52:35Z" }, { - "quantity" : 115, - "startDate" : "2023-06-22T23:57:34Z" + "value" : 115, + "date" : "2023-06-22T23:57:34Z" }, { - "quantity" : 116, - "startDate" : "2023-06-23T00:02:34Z" + "value" : 116, + "date" : "2023-06-23T00:02:34Z" }, { - "quantity" : 133, - "startDate" : "2023-06-23T00:07:34Z" + "value" : 133, + "date" : "2023-06-23T00:07:34Z" }, { - "quantity" : 145, - "startDate" : "2023-06-23T00:12:34Z" + "value" : 145, + "date" : "2023-06-23T00:12:34Z" }, { - "quantity" : 140, - "startDate" : "2023-06-23T00:17:34Z" + "value" : 140, + "date" : "2023-06-23T00:17:34Z" }, { - "quantity" : 161, - "startDate" : "2023-06-23T00:22:35Z" + "value" : 161, + "date" : "2023-06-23T00:22:35Z" }, { - "quantity" : 166, - "startDate" : "2023-06-23T00:27:34Z" + "value" : 166, + "date" : "2023-06-23T00:27:34Z" }, { - "quantity" : 172, - "startDate" : "2023-06-23T00:32:35Z" + "value" : 172, + "date" : "2023-06-23T00:32:35Z" }, { - "quantity" : 182, - "startDate" : "2023-06-23T00:37:35Z" + "value" : 182, + "date" : "2023-06-23T00:37:35Z" }, { - "quantity" : 184, - "startDate" : "2023-06-23T00:42:35Z" + "value" : 184, + "date" : "2023-06-23T00:42:35Z" }, { - "quantity" : 185, - "startDate" : "2023-06-23T00:47:34Z" + "value" : 185, + "date" : "2023-06-23T00:47:34Z" }, { - "quantity" : 190, - "startDate" : "2023-06-23T00:52:35Z" + "value" : 190, + "date" : "2023-06-23T00:52:35Z" }, { - "quantity" : 182, - "startDate" : "2023-06-23T00:57:34Z" + "value" : 182, + "date" : "2023-06-23T00:57:34Z" }, { - "quantity" : 166, - "startDate" : "2023-06-23T01:02:35Z" + "value" : 166, + "date" : "2023-06-23T01:02:35Z" }, { - "quantity" : 174, - "startDate" : "2023-06-23T01:07:34Z" + "value" : 174, + "date" : "2023-06-23T01:07:34Z" }, { - "quantity" : 179, - "startDate" : "2023-06-23T01:12:34Z" + "value" : 179, + "date" : "2023-06-23T01:12:34Z" }, { - "quantity" : 166, - "startDate" : "2023-06-23T01:17:35Z" + "value" : 166, + "date" : "2023-06-23T01:17:35Z" }, { - "quantity" : 134, - "startDate" : "2023-06-23T01:22:34Z" + "value" : 134, + "date" : "2023-06-23T01:22:34Z" }, { - "quantity" : 131, - "startDate" : "2023-06-23T01:27:35Z" + "value" : 131, + "date" : "2023-06-23T01:27:35Z" }, { - "quantity" : 129, - "startDate" : "2023-06-23T01:32:34Z" + "value" : 129, + "date" : "2023-06-23T01:32:34Z" }, { - "quantity" : 136, - "startDate" : "2023-06-23T01:37:34Z" + "value" : 136, + "date" : "2023-06-23T01:37:34Z" }, { - "quantity" : 152, - "startDate" : "2023-06-23T01:42:34Z" + "value" : 152, + "date" : "2023-06-23T01:42:34Z" }, { - "quantity" : 162, - "startDate" : "2023-06-23T01:47:35Z" + "value" : 162, + "date" : "2023-06-23T01:47:35Z" }, { - "quantity" : 165, - "startDate" : "2023-06-23T01:52:34Z" + "value" : 165, + "date" : "2023-06-23T01:52:34Z" }, { - "quantity" : 172, - "startDate" : "2023-06-23T01:57:34Z" + "value" : 172, + "date" : "2023-06-23T01:57:34Z" }, { - "quantity" : 176, - "startDate" : "2023-06-23T02:02:35Z" + "value" : 176, + "date" : "2023-06-23T02:02:35Z" }, { - "quantity" : 165, - "startDate" : "2023-06-23T02:07:35Z" + "value" : 165, + "date" : "2023-06-23T02:07:35Z" }, { - "quantity" : 172, - "startDate" : "2023-06-23T02:12:34Z" + "value" : 172, + "date" : "2023-06-23T02:12:34Z" }, { - "quantity" : 170, - "startDate" : "2023-06-23T02:17:35Z" + "value" : 170, + "date" : "2023-06-23T02:17:35Z" }, { - "quantity" : 177, - "startDate" : "2023-06-23T02:22:35Z" + "value" : 177, + "date" : "2023-06-23T02:22:35Z" }, { - "quantity" : 176, - "startDate" : "2023-06-23T02:27:35Z" + "value" : 176, + "date" : "2023-06-23T02:27:35Z" }, { - "quantity" : 173, - "startDate" : "2023-06-23T02:32:34Z" + "value" : 173, + "date" : "2023-06-23T02:32:34Z" }, { - "quantity" : 180, - "startDate" : "2023-06-23T02:37:35Z" + "value" : 180, + "date" : "2023-06-23T02:37:35Z" } ], "basal" : [ diff --git a/LoopTests/Fixtures/live_capture/live_capture_predicted_glucose.json b/LoopTests/Fixtures/live_capture/live_capture_predicted_glucose.json index a98fbaccb7..b77cb55868 100644 --- a/LoopTests/Fixtures/live_capture/live_capture_predicted_glucose.json +++ b/LoopTests/Fixtures/live_capture/live_capture_predicted_glucose.json @@ -10,382 +10,382 @@ "startDate" : "2023-06-23T02:40:00Z" }, { - "quantity" : 180.51458820506667, + "quantity" : 180.52987493690765, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T02:45:00Z" }, { - "quantity" : 179.7158986124237, + "quantity" : 179.77931710835796, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T02:50:00Z" }, { - "quantity" : 177.66868460973922, + "quantity" : 177.81435588000684, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T02:55:00Z" }, { - "quantity" : 174.80252509117634, + "quantity" : 175.04920382978105, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T03:00:00Z" }, { - "quantity" : 171.74984493231631, + "quantity" : 172.09884468881066, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T03:05:00Z" }, { - "quantity" : 168.58187755437024, + "quantity" : 169.0341959170697, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T03:10:00Z" }, { - "quantity" : 165.36216340804185, + "quantity" : 165.91852357330802, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T03:15:00Z" }, { - "quantity" : 162.12697210734922, + "quantity" : 162.78787379965794, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T03:20:00Z" }, { - "quantity" : 158.90986429144345, + "quantity" : 159.67566374385987, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T03:25:00Z" }, { - "quantity" : 155.75684851046043, + "quantity" : 156.6278000530812, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T03:30:00Z" }, { - "quantity" : 152.70869296700107, + "quantity" : 153.68497899133908, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T03:35:00Z" }, { - "quantity" : 149.78068888956841, + "quantity" : 150.85857622089654, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T03:40:00Z" }, { - "quantity" : 147.00401242102828, + "quantity" : 148.1797464838103, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T03:45:00Z" }, { - "quantity" : 144.40563853768242, + "quantity" : 145.67546444468488, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T03:50:00Z" }, { - "quantity" : 142.0087170601098, + "quantity" : 143.36889813413907, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T03:55:00Z" }, { - "quantity" : 139.83295658233396, + "quantity" : 141.27978455565565, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T04:00:00Z" }, { - "quantity" : 137.89511837124121, + "quantity" : 139.4249156157845, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T04:05:00Z" }, { - "quantity" : 136.07526338088792, + "quantity" : 137.7082164432302, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T04:10:00Z" }, { - "quantity" : 134.25815754225141, + "quantity" : 135.9914530272836, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T04:15:00Z" }, { - "quantity" : 132.45275084533137, + "quantity" : 134.2827664300858, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T04:20:00Z" }, { - "quantity" : 130.66563522056958, + "quantity" : 132.58882252103788, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T04:25:00Z" }, { - "quantity" : 128.90146920949769, + "quantity" : 130.91436540926705, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T04:30:00Z" }, { - "quantity" : 127.16322092092855, + "quantity" : 129.26245506698106, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T04:35:00Z" }, { - "quantity" : 125.45215396105368, + "quantity" : 127.63445215517064, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T04:40:00Z" }, { - "quantity" : 123.76712483433676, + "quantity" : 126.02931442610466, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T04:45:00Z" }, { - "quantity" : 122.10683165409341, + "quantity" : 124.44584453318035, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T04:50:00Z" }, { - "quantity" : 120.46857875163471, + "quantity" : 122.88145382927624, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T04:55:00Z" }, { - "quantity" : 118.84903308222181, + "quantity" : 121.33291804466413, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T05:00:00Z" }, { - "quantity" : 117.24445077397047, + "quantity" : 119.79660318395023, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T05:05:00Z" }, { - "quantity" : 115.65043839655846, + "quantity" : 118.26822621269756, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T05:10:00Z" }, { - "quantity" : 114.06198688414838, + "quantity" : 116.74288846240054, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T05:15:00Z" }, { - "quantity" : 112.47356001340279, + "quantity" : 115.21516364934988, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T05:20:00Z" }, { - "quantity" : 110.87917488553444, + "quantity" : 113.67917795139525, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T05:25:00Z" }, { - "quantity" : 109.27247502015473, + "quantity" : 112.12868274578355, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T05:30:00Z" }, { - "quantity" : 107.64679662666447, + "quantity" : 110.55712056957398, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T05:35:00Z" }, { - "quantity" : 105.99522857963143, + "quantity" : 108.95768482515078, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T05:40:00Z" }, { - "quantity" : 104.31066658787131, + "quantity" : 107.32337371691418, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T05:45:00Z" }, { - "quantity" : 102.58586201263279, + "quantity" : 105.64703887119052, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T05:50:00Z" }, { - "quantity" : 100.81350120847731, + "quantity" : 103.92146136061618, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T05:55:00Z" }, { - "quantity" : 98.986445102805988, + "quantity" : 102.13957364029821, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T06:00:00Z" }, { - "quantity" : 97.097518927124952, + "quantity" : 100.29425666336888, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T06:05:00Z" }, { - "quantity" : 95.139330662672023, + "quantity" : 98.37810372588095, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T06:10:00Z" }, { - "quantity" : 93.104670202578632, + "quantity" : 96.38393930539169, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T06:15:00Z" }, { - "quantity" : 90.986165185301502, + "quantity" : 94.30446350902744, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T06:20:00Z" }, { - "quantity" : 88.909927040807588, + "quantity" : 92.24204127278486, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T06:25:00Z" }, { - "quantity" : 86.994338611676767, + "quantity" : 90.33818302395392, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T06:30:00Z" }, { - "quantity" : 85.232136877351081, + "quantity" : 88.58657375772682, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T06:35:00Z" }, { - "quantity" : 83.615651290380811, + "quantity" : 86.9796355549934, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T06:40:00Z" }, { - "quantity" : 82.136746744082188, + "quantity" : 85.50932186775859, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T06:45:00Z" }, { - "quantity" : 80.787935960558002, + "quantity" : 84.16822997919033, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T06:50:00Z" }, { - "quantity" : 79.561150334091622, + "quantity" : 82.94837192653554, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T06:55:00Z" }, { - "quantity" : 78.448809315519384, + "quantity" : 81.84224397138112, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T07:00:00Z" }, { - "quantity" : 77.444295000376087, + "quantity" : 80.8433012790305, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T07:05:00Z" }, { - "quantity" : 76.541144021775267, + "quantity" : 79.94514990703274, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T07:10:00Z" }, { - "quantity" : 75.734033247701291, + "quantity" : 79.1425285689858, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T07:15:00Z" }, { - "quantity" : 75.018229944400559, + "quantity" : 78.43073701607969, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T07:20:00Z" }, { - "quantity" : 74.389076912965834, + "quantity" : 77.80513210408813, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T07:25:00Z" }, { - "quantity" : 73.841309919727451, + "quantity" : 77.26038909817899, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T07:30:00Z" }, { - "quantity" : 73.370549918316215, + "quantity" : 76.79214128522554, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T07:35:00Z" }, { - "quantity" : 72.972744055408953, + "quantity" : 76.39636603545401, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T07:40:00Z" }, { - "quantity" : 72.643975082565134, + "quantity" : 76.06917517261084, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T07:45:00Z" }, { - "quantity" : 72.380461060355856, + "quantity" : 75.80681469169488, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T07:50:00Z" }, { - "quantity" : 72.178520063294286, + "quantity" : 75.60563685065486, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T07:55:00Z" }, { - "quantity" : 72.034174053629386, + "quantity" : 75.46174433219417, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T08:00:00Z" }, { - "quantity" : 71.942299096190823, + "quantity" : 75.3700976935867, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T08:05:00Z" }, { - "quantity" : 71.897751011456421, + "quantity" : 75.32563190200372, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T08:10:00Z" }, { - "quantity" : 71.895123880236383, + "quantity" : 75.32301505961473, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T08:15:00Z" }, { - "quantity" : 71.906254842464136, + "quantity" : 75.33414614640142, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T08:20:00Z" }, { - "quantity" : 71.914434937142801, + "quantity" : 75.34232624108009, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T08:25:00Z" }, { - "quantity" : 71.920167940771535, + "quantity" : 75.34805924470882, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T08:30:00Z" }, { - "quantity" : 71.923927819981145, + "quantity" : 75.35181912391843, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T08:35:00Z" }, { - "quantity" : 71.926159114246957, + "quantity" : 75.35405041818424, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T08:40:00Z" }, { - "quantity" : 71.927280081079402, + "quantity" : 75.35517138501669, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T08:45:00Z" }, { - "quantity" : 71.927682355083221, + "quantity" : 75.35557365902051, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T08:50:00Z" }, { - "quantity" : 71.927731342958282, + "quantity" : 75.35562264689557, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T08:55:00Z" }, { - "quantity" : 71.927731342958282, + "quantity" : 75.35562264689557, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T09:00:00Z" } diff --git a/LoopTests/Managers/DeviceDataManagerTests.swift b/LoopTests/Managers/DeviceDataManagerTests.swift index 6872bf9590..f8f68b841f 100644 --- a/LoopTests/Managers/DeviceDataManagerTests.swift +++ b/LoopTests/Managers/DeviceDataManagerTests.swift @@ -10,6 +10,7 @@ import XCTest import HealthKit import LoopKit import LoopKitUI +import LoopCore @testable import Loop @MainActor @@ -50,17 +51,13 @@ final class DeviceDataManagerTests: XCTestCase { let healthStore = HKHealthStore() - let carbAbsorptionTimes: CarbStore.DefaultAbsorptionTimes = (fast: .minutes(30), medium: .hours(3), slow: .hours(5)) - let carbStore = CarbStore( cacheStore: persistenceController, - cacheLength: .days(1), - defaultAbsorptionTimes: carbAbsorptionTimes + cacheLength: .days(1) ) let doseStore = DoseStore( - cacheStore: persistenceController, - insulinModelProvider: PresetInsulinModelProvider(defaultRapidActingModel: nil) + cacheStore: persistenceController ) let glucoseStore = GlucoseStore(cacheStore: persistenceController) diff --git a/LoopTests/Managers/DoseEnactorTests.swift b/LoopTests/Managers/DoseEnactorTests.swift index eddfac1a9a..08e5f4d9b5 100644 --- a/LoopTests/Managers/DoseEnactorTests.swift +++ b/LoopTests/Managers/DoseEnactorTests.swift @@ -10,6 +10,7 @@ import XCTest import Foundation import LoopKit import HealthKit +import LoopAlgorithm @testable import Loop diff --git a/LoopTests/Managers/LoopAlgorithmTests.swift b/LoopTests/Managers/LoopAlgorithmTests.swift deleted file mode 100644 index e63f86bb46..0000000000 --- a/LoopTests/Managers/LoopAlgorithmTests.swift +++ /dev/null @@ -1,224 +0,0 @@ -// -// 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() { - // This matches the "testForecastFromLiveCaptureInputData" test of LoopDataManagerDosingTests, - // Using the same input data, but generating the forecast using the LoopAlgorithm.generatePrediction() - // function. - - let decoder = JSONDecoder() - decoder.dateDecodingStrategy = .iso8601 - let url = bundle.url(forResource: "live_capture_input", withExtension: "json")! - let input = try! decoder.decode(LoopPredictionInput.self, from: try! Data(contentsOf: url)) - - let prediction = LoopAlgorithm.generatePrediction( - start: input.glucoseHistory.last?.startDate ?? Date(), - glucoseHistory: input.glucoseHistory, - doses: input.doses, - carbEntries: input.carbEntries, - basal: input.basal, - sensitivity: input.sensitivity, - carbRatio: input.carbRatio, - useIntegralRetrospectiveCorrection: input.useIntegralRetrospectiveCorrection - ) - - 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) - } - } - - func testAutoBolusMaxIOBClamping() async { - let now = ISO8601DateFormatter().date(from: "2020-03-11T12:13:14-0700")! - - var input = LoopAlgorithmInput.mock(for: now) - input.recommendationType = .automaticBolus - - // 8U bolus on board, and 100g carbs; CR = 10, so that should be 10U to cover the carbs - input.doses = [DoseEntry(type: .bolus, startDate: now.addingTimeInterval(-.minutes(5)), value: 8, unit: .units)] - input.carbEntries = [ - StoredCarbEntry(startDate: now.addingTimeInterval(.minutes(-5)), quantity: .carbs(value: 100)) - ] - - // Max activeInsulin = 2 x maxBolus = 16U - input.maxBolus = 8 - var output = LoopAlgorithm.run(input: input) - var recommendedBolus = output.recommendation!.automatic?.bolusUnits - var activeInsulin = output.activeInsulin! - XCTAssertEqual(activeInsulin, 8.0) - XCTAssertEqual(recommendedBolus!, 1.71, accuracy: 0.01) - - // Now try with maxBolus of 4; should not recommend any more insulin, as we're at our max iob - input.maxBolus = 4 - output = LoopAlgorithm.run(input: input) - recommendedBolus = output.recommendation!.automatic?.bolusUnits - activeInsulin = output.activeInsulin! - XCTAssertEqual(activeInsulin, 8.0) - XCTAssertEqual(recommendedBolus!, 0, accuracy: 0.01) - } - - func testTempBasalMaxIOBClamping() { - let now = ISO8601DateFormatter().date(from: "2020-03-11T12:13:14-0700")! - - var input = LoopAlgorithmInput.mock(for: now) - input.recommendationType = .tempBasal - - // 8U bolus on board, and 100g carbs; CR = 10, so that should be 10U to cover the carbs - input.doses = [DoseEntry(type: .bolus, startDate: now.addingTimeInterval(-.minutes(5)), value: 8, unit: .units)] - input.carbEntries = [ - StoredCarbEntry(startDate: now.addingTimeInterval(.minutes(-5)), quantity: .carbs(value: 100)) - ] - - // Max activeInsulin = 2 x maxBolus = 16U - input.maxBolus = 8 - var output = LoopAlgorithm.run(input: input) - var recommendedRate = output.recommendation!.automatic!.basalAdjustment!.unitsPerHour - var activeInsulin = output.activeInsulin! - XCTAssertEqual(activeInsulin, 8.0) - XCTAssertEqual(recommendedRate, 8.0, accuracy: 0.01) - - // Now try with maxBolus of 4; should only recommend scheduled basal (1U/hr), as we're at our max iob - input.maxBolus = 4 - output = LoopAlgorithm.run(input: input) - recommendedRate = output.recommendation!.automatic!.basalAdjustment!.unitsPerHour - activeInsulin = output.activeInsulin! - XCTAssertEqual(activeInsulin, 8.0) - XCTAssertEqual(recommendedRate, 1.0, accuracy: 0.01) - } -} - - -extension LoopAlgorithmInput { - static func mock(for date: Date, glucose: [Double] = [100, 120, 140, 160]) -> LoopAlgorithmInput { - - func d(_ interval: TimeInterval) -> Date { - return date.addingTimeInterval(interval) - } - - var input = LoopAlgorithmInput( - predictionStart: date, - glucoseHistory: [], - doses: [], - carbEntries: [], - basal: [], - sensitivity: [], - carbRatio: [], - target: [], - suspendThreshold: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 65), - maxBolus: 6, - maxBasalRate: 8, - recommendationInsulinType: .novolog, - recommendationType: .automaticBolus - ) - - for (idx, value) in glucose.enumerated() { - let entry = StoredGlucoseSample(startDate: d(.minutes(Double(-(glucose.count - idx)*5)) + .minutes(1)), quantity: .glucose(value: value)) - input.glucoseHistory.append(entry) - } - - input.doses = [ - DoseEntry(type: .bolus, startDate: d(.minutes(-3)), value: 1.0, unit: .units) - ] - - input.carbEntries = [ - StoredCarbEntry(startDate: d(.minutes(-4)), quantity: .carbs(value: 20)) - ] - - let forecastEndTime = date.addingTimeInterval(InsulinMath.defaultInsulinActivityDuration).dateCeiledToTimeInterval(.minutes(GlucoseMath.defaultDelta)) - let dosesStart = date.addingTimeInterval(-(CarbMath.maximumAbsorptionTimeInterval + InsulinMath.defaultInsulinActivityDuration)) - let carbsStart = date.addingTimeInterval(-CarbMath.maximumAbsorptionTimeInterval) - - - let basalRateSchedule = BasalRateSchedule( - dailyItems: [ - RepeatingScheduleValue(startTime: 0, value: 1), - ], - timeZone: .utcTimeZone - )! - input.basal = basalRateSchedule.between(start: dosesStart, end: date) - - let insulinSensitivitySchedule = InsulinSensitivitySchedule( - unit: .milligramsPerDeciliter, - dailyItems: [ - RepeatingScheduleValue(startTime: 0, value: 45), - RepeatingScheduleValue(startTime: 32400, value: 55) - ], - timeZone: .utcTimeZone - )! - input.sensitivity = insulinSensitivitySchedule.quantitiesBetween(start: dosesStart, end: forecastEndTime) - - let carbRatioSchedule = CarbRatioSchedule( - unit: .gram(), - dailyItems: [ - RepeatingScheduleValue(startTime: 0.0, value: 10.0), - ], - timeZone: .utcTimeZone - )! - input.carbRatio = carbRatioSchedule.between(start: carbsStart, end: date) - - let targetSchedule = GlucoseRangeSchedule( - unit: .milligramsPerDeciliter, - dailyItems: [ - RepeatingScheduleValue(startTime: 0, value: DoubleRange(minValue: 100, maxValue: 110)), - ], - timeZone: .utcTimeZone - )! - input.target = targetSchedule.quantityBetween(start: date, end: forecastEndTime) - return input - } -} - diff --git a/LoopTests/Managers/LoopDataManagerTests.swift b/LoopTests/Managers/LoopDataManagerTests.swift index 2819956f23..d9fa9aaf31 100644 --- a/LoopTests/Managers/LoopDataManagerTests.swift +++ b/LoopTests/Managers/LoopDataManagerTests.swift @@ -10,6 +10,8 @@ import XCTest import HealthKit import LoopKit import HealthKit +import LoopAlgorithm + @testable import LoopCore @testable import Loop @@ -201,14 +203,14 @@ class LoopDataManagerTests: XCTestCase { automaticDosingStrategy: .automaticBolus ) - glucoseStore.storedGlucose = predictionInput.glucoseHistory + glucoseStore.storedGlucose = predictionInput.glucoseHistory.map { StoredGlucoseSample.from(fixture: $0) } let currentDate = glucoseStore.latestGlucose!.startDate now = currentDate - doseStore.doseHistory = predictionInput.doses + doseStore.doseHistory = predictionInput.doses.map { DoseEntry.from(fixture: $0) } doseStore.lastAddedPumpData = predictionInput.doses.last!.startDate - carbStore.carbHistory = predictionInput.carbEntries + carbStore.carbHistory = predictionInput.carbEntries.map { StoredCarbEntry.from(fixture: $0) } let expectedPredictedGlucose = loadPredictedGlucoseFixture("live_capture_predicted_glucose") @@ -258,13 +260,13 @@ class LoopDataManagerTests: XCTestCase { await loopDataManager.updateDisplayState() - XCTAssertEqual(150, loopDataManager.eventualBG) + XCTAssertEqual(132, loopDataManager.eventualBG!, accuracy: 0.5) XCTAssert(!loopDataManager.displayState.output!.effects.momentum.isEmpty) await loopDataManager.loop() // Should correct high. - XCTAssertEqual(0.4, deliveryDelegate.lastEnact!.bolusUnits!, accuracy: defaultAccuracy) + XCTAssertEqual(0.25, deliveryDelegate.lastEnact!.bolusUnits!, accuracy: defaultAccuracy) } func testHighAndRisingWithCOB() async { @@ -277,13 +279,13 @@ class LoopDataManagerTests: XCTestCase { await loopDataManager.updateDisplayState() - XCTAssertEqual(250, loopDataManager.eventualBG) + XCTAssertEqual(268, loopDataManager.eventualBG!, accuracy: 0.5) XCTAssert(!loopDataManager.displayState.output!.effects.momentum.isEmpty) await loopDataManager.loop() // Should correct high. - XCTAssertEqual(1.15, deliveryDelegate.lastEnact!.bolusUnits!, accuracy: defaultAccuracy) + XCTAssertEqual(1.25, deliveryDelegate.lastEnact!.bolusUnits!, accuracy: defaultAccuracy) } func testLowAndFalling() async { @@ -296,7 +298,7 @@ class LoopDataManagerTests: XCTestCase { await loopDataManager.updateDisplayState() - XCTAssertEqual(75, loopDataManager.eventualBG!, accuracy: 1.0) + XCTAssertEqual(66, loopDataManager.eventualBG!, accuracy: 0.5) XCTAssert(!loopDataManager.displayState.output!.effects.momentum.isEmpty) await loopDataManager.loop() @@ -311,8 +313,8 @@ class LoopDataManagerTests: XCTestCase { glucoseStore.storedGlucose = [ StoredGlucoseSample(startDate: d(.minutes(-18)), quantity: .glucose(value: 100)), StoredGlucoseSample(startDate: d(.minutes(-13)), quantity: .glucose(value: 95)), - StoredGlucoseSample(startDate: d(.minutes(-8)), quantity: .glucose(value: 90)), - StoredGlucoseSample(startDate: d(.minutes(-3)), quantity: .glucose(value: 85)), + StoredGlucoseSample(startDate: d(.minutes(-8)), quantity: .glucose(value: 92)), + StoredGlucoseSample(startDate: d(.minutes(-3)), quantity: .glucose(value: 90)), ] carbStore.carbHistory = [ @@ -321,7 +323,7 @@ class LoopDataManagerTests: XCTestCase { await loopDataManager.updateDisplayState() - XCTAssertEqual(185, loopDataManager.eventualBG!, accuracy: 1.0) + XCTAssertEqual(192, loopDataManager.eventualBG!, accuracy: 0.5) XCTAssert(!loopDataManager.displayState.output!.effects.momentum.isEmpty) await loopDataManager.loop() @@ -372,6 +374,47 @@ class LoopDataManagerTests: XCTestCase { } } + func testOngoingTempBasalIsSufficient() async { + // LoopDataManager should trim future temp basals when running the algorithm. + // and should not include effects from future delivery of the temp basal in its prediction. + + glucoseStore.storedGlucose = [ + StoredGlucoseSample(startDate: d(.minutes(-4)), quantity: .glucose(value: 100)), + ] + + carbStore.carbHistory = [ + StoredCarbEntry(startDate: d(.minutes(-5)), quantity: .carbs(value: 20)) + ] + + // Temp basal started one minute ago, covering carbs. + let dose = DoseEntry( + type: .tempBasal, + startDate: d(.minutes(-1)), + endDate: d(.minutes(29)), + value: 5.05, + unit: .unitsPerHour + ) + deliveryDelegate.basalDeliveryState = .tempBasal(dose) + + doseStore.doseHistory = [ dose ] + + settingsProvider.settings.automaticDosingStrategy = .tempBasalOnly + + await loopDataManager.loop() + + // Should not adjust delivery, as existing temp basal is correct. + let expectedAutomaticDoseRecommendation = AutomaticDoseRecommendation(basalAdjustment: nil) + XCTAssertNil(deliveryDelegate.lastEnact) + XCTAssertEqual(dosingDecisionStore.dosingDecisions.count, 1) + if dosingDecisionStore.dosingDecisions.count == 1 { + XCTAssertEqual(dosingDecisionStore.dosingDecisions[0].reason, "loop") + XCTAssertEqual(dosingDecisionStore.dosingDecisions[0].automaticDoseRecommendation, expectedAutomaticDoseRecommendation) + XCTAssertNil(dosingDecisionStore.dosingDecisions[0].manualBolusRecommendation) + XCTAssertNil(dosingDecisionStore.dosingDecisions[0].manualBolusRequested) + } + } + + func testLoopRecommendsTempBasalWithoutEnactingIfOpenLoop() async { glucoseStore.storedGlucose = [ StoredGlucoseSample(startDate: d(.minutes(-1)), quantity: .glucose(value: 150)), @@ -400,7 +443,7 @@ class LoopDataManagerTests: XCTestCase { loopDataManager.usePositiveMomentumAndRCForManualBoluses = true var recommendation = try! await loopDataManager.recommendManualBolus()! - XCTAssertEqual(recommendation.amount, 2.46, accuracy: 0.01) + XCTAssertEqual(recommendation.amount, 3.44, accuracy: 0.01) loopDataManager.usePositiveMomentumAndRCForManualBoluses = false recommendation = try! await loopDataManager.recommendManualBolus()! @@ -448,3 +491,39 @@ extension LoopDataManager { displayState.output?.predictedGlucose.last?.quantity.doubleValue(for: .milligramsPerDeciliter) } } + +extension StoredGlucoseSample { + static func from(fixture: FixtureGlucoseSample) -> StoredGlucoseSample { + return StoredGlucoseSample( + startDate: fixture.startDate, + quantity: fixture.quantity, + condition: fixture.condition, + trendRate: fixture.trendRate, + isDisplayOnly: fixture.isDisplayOnly, + wasUserEntered: fixture.wasUserEntered + ) + } +} + +extension DoseEntry { + static func from(fixture: FixtureInsulinDose) -> DoseEntry { + return DoseEntry( + type: fixture.deliveryType == .bolus ? .bolus : .basal, + startDate: fixture.startDate, + endDate: fixture.endDate, + value: fixture.volume, + unit: .units + ) + } +} + +extension StoredCarbEntry { + static func from(fixture: FixtureCarbEntry) -> StoredCarbEntry { + return StoredCarbEntry( + startDate: fixture.startDate, + quantity: fixture.quantity, + foodType: fixture.foodType, + absorptionTime: fixture.absorptionTime + ) + } +} diff --git a/LoopTests/Managers/MealDetectionManagerTests.swift b/LoopTests/Managers/MealDetectionManagerTests.swift index 2148821f54..5b97629de5 100644 --- a/LoopTests/Managers/MealDetectionManagerTests.swift +++ b/LoopTests/Managers/MealDetectionManagerTests.swift @@ -10,6 +10,8 @@ import XCTest import HealthKit import LoopCore import LoopKit +import LoopAlgorithm + @testable import Loop fileprivate class MockGlucoseSample: GlucoseSampleValue { @@ -17,7 +19,7 @@ fileprivate class MockGlucoseSample: GlucoseSampleValue { let provenanceIdentifier = "" let isDisplayOnly: Bool let wasUserEntered: Bool - let condition: LoopKit.GlucoseCondition? = nil + let condition: GlucoseCondition? = nil let trendRate: HKQuantity? = nil var trend: LoopKit.GlucoseTrend? var syncIdentifier: String? @@ -191,8 +193,8 @@ class MealDetectionManagerTests: XCTestCase { mealDetectionManager.test_currentDate! } - var algorithmInput: LoopAlgorithmInput! - var algorithmOutput: LoopAlgorithmOutput! + var algorithmInput: StoredDataAlgorithmInput! + var algorithmOutput: AlgorithmOutput! var mockAlgorithmState: AlgorithmDisplayState! @@ -216,11 +218,11 @@ class MealDetectionManagerTests: XCTestCase { insulinSensitivityScheduleApplyingOverrideHistory = testType.insulinSensitivitySchedule carbRatioSchedule = testType.carbSchedule - algorithmInput = LoopAlgorithmInput( - predictionStart: date, + algorithmInput = StoredDataAlgorithmInput( glucoseHistory: [StoredGlucoseSample(startDate: date, quantity: .init(unit: .milligramsPerDeciliter, doubleValue: 100))], doses: [], carbEntries: testType.carbEntries.map { $0.asStoredCarbEntry }, + predictionStart: date, basal: BasalRateSchedule(dailyItems: [RepeatingScheduleValue(startTime: 0, value: 1.0)])!.between(start: historyStart, end: date), sensitivity: testType.insulinSensitivitySchedule.quantitiesBetween(start: historyStart, end: date), carbRatio: testType.carbSchedule.between(start: historyStart, end: date), @@ -228,7 +230,10 @@ class MealDetectionManagerTests: XCTestCase { suspendThreshold: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 65), maxBolus: maximumBolus!, maxBasalRate: maximumBasalRatePerHour, - recommendationInsulinType: .novolog, + useIntegralRetrospectiveCorrection: false, + includePositiveVelocityAndRC: true, + carbAbsorptionModel: .piecewiseLinear, + recommendationInsulinModel: ExponentialInsulinModelPreset.rapidActingAdult.model, recommendationType: .automaticBolus ) @@ -260,7 +265,7 @@ class MealDetectionManagerTests: XCTestCase { retrospectiveGlucoseDiscrepancies: [] ) - algorithmOutput = LoopAlgorithmOutput( + algorithmOutput = AlgorithmOutput( recommendationResult: .success(.init()), predictedGlucose: [], effects: effects, diff --git a/LoopTests/Managers/TemporaryPresetsManagerTests.swift b/LoopTests/Managers/TemporaryPresetsManagerTests.swift index 60da1a21c2..cb79a3878d 100644 --- a/LoopTests/Managers/TemporaryPresetsManagerTests.swift +++ b/LoopTests/Managers/TemporaryPresetsManagerTests.swift @@ -8,6 +8,7 @@ import XCTest import LoopKit + @testable import Loop diff --git a/LoopTests/Mock Stores/MockDoseStore.swift b/LoopTests/Mock Stores/MockDoseStore.swift index 985ac687fe..061d258e05 100644 --- a/LoopTests/Mock Stores/MockDoseStore.swift +++ b/LoopTests/Mock Stores/MockDoseStore.swift @@ -8,6 +8,7 @@ import HealthKit import LoopKit +import LoopAlgorithm @testable import Loop class MockDoseStore: DoseStoreProtocol { @@ -23,7 +24,7 @@ class MockDoseStore: DoseStoreProtocol { var lastReservoirValue: LoopKit.ReservoirValue? - func getTotalUnitsDelivered(since startDate: Date) async throws -> LoopKit.InsulinValue { + func getTotalUnitsDelivered(since startDate: Date) async throws -> InsulinValue { return InsulinValue(startDate: lastAddedPumpData, value: 0) } diff --git a/LoopTests/Mock Stores/MockGlucoseStore.swift b/LoopTests/Mock Stores/MockGlucoseStore.swift index 064f3c0fba..ea6c3f118d 100644 --- a/LoopTests/Mock Stores/MockGlucoseStore.swift +++ b/LoopTests/Mock Stores/MockGlucoseStore.swift @@ -8,6 +8,7 @@ import HealthKit import LoopKit +import LoopAlgorithm @testable import Loop class MockGlucoseStore: GlucoseStoreProtocol { diff --git a/LoopTests/Mocks/MockDeliveryDelegate.swift b/LoopTests/Mocks/MockDeliveryDelegate.swift index bc14f03f00..c3bd8e911b 100644 --- a/LoopTests/Mocks/MockDeliveryDelegate.swift +++ b/LoopTests/Mocks/MockDeliveryDelegate.swift @@ -8,6 +8,7 @@ import Foundation import LoopKit +import LoopAlgorithm @testable import Loop class MockDeliveryDelegate: DeliveryDelegate { diff --git a/LoopTests/Mocks/MockSettingsProvider.swift b/LoopTests/Mocks/MockSettingsProvider.swift index 150608a1fe..4fcfe6e34f 100644 --- a/LoopTests/Mocks/MockSettingsProvider.swift +++ b/LoopTests/Mocks/MockSettingsProvider.swift @@ -9,6 +9,7 @@ import Foundation import LoopKit import HealthKit +import LoopAlgorithm @testable import Loop class MockSettingsProvider: SettingsProvider { diff --git a/LoopTests/Models/TempBasalRecommendationTests.swift b/LoopTests/Models/TempBasalRecommendationTests.swift new file mode 100644 index 0000000000..8c0c7ab1f4 --- /dev/null +++ b/LoopTests/Models/TempBasalRecommendationTests.swift @@ -0,0 +1,26 @@ +// +// TempBasalRecommendationTests.swift +// LoopTests +// +// Created by Pete Schwamb on 2/21/24. +// Copyright © 2024 LoopKit Authors. All rights reserved. +// + +import XCTest +import LoopAlgorithm +@testable import Loop + +class TempBasalRecommendationTests: XCTestCase { + + func testCancel() { + let cancel = TempBasalRecommendation.cancel + XCTAssertEqual(cancel.unitsPerHour, 0) + XCTAssertEqual(cancel.duration, 0) + } + + func testInitializer() { + let tempBasalRecommendation = TempBasalRecommendation(unitsPerHour: 1.23, duration: 4.56) + XCTAssertEqual(tempBasalRecommendation.unitsPerHour, 1.23) + XCTAssertEqual(tempBasalRecommendation.duration, 4.56) + } +} diff --git a/LoopTests/ViewModels/BolusEntryViewModelTests.swift b/LoopTests/ViewModels/BolusEntryViewModelTests.swift index f5667f2857..05cac52a87 100644 --- a/LoopTests/ViewModels/BolusEntryViewModelTests.swift +++ b/LoopTests/ViewModels/BolusEntryViewModelTests.swift @@ -12,6 +12,8 @@ import LoopKit import LoopKitUI import SwiftUI import XCTest +import LoopAlgorithm + @testable import Loop @MainActor @@ -21,7 +23,7 @@ class BolusEntryViewModelTests: XCTestCase { static let now = ISO8601DateFormatter().date(from: "2020-03-11T07:00:00-0700")! static let exampleStartDate = now - .hours(2) static let exampleEndDate = now - .hours(1) - static fileprivate let exampleGlucoseValue = MockGlucoseValue(quantity: exampleManualGlucoseQuantity, startDate: exampleStartDate) + static fileprivate let exampleGlucoseValue = SimpleGlucoseValue(startDate: exampleStartDate, quantity: exampleManualGlucoseQuantity) static let exampleManualGlucoseQuantity = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 123.4) static let exampleManualGlucoseSample = HKQuantitySample(type: HKQuantityType.quantityType(forIdentifier: .bloodGlucose)!, @@ -828,6 +830,9 @@ public enum BolusEntryViewTestError: Error { } fileprivate class MockBolusEntryViewModelDelegate: BolusEntryViewModelDelegate { + func insulinModel(for type: LoopKit.InsulinType?) -> InsulinModel { + return ExponentialInsulinModelPreset.rapidActingAdult + } var settings = StoredSettings( dosingEnabled: true, @@ -848,17 +853,17 @@ fileprivate class MockBolusEntryViewModelDelegate: BolusEntryViewModelDelegate { var preMealOverride: LoopKit.TemporaryScheduleOverride? - var pumpInsulinType: LoopKit.InsulinType? + var pumpInsulinType: InsulinType? var mostRecentGlucoseDataDate: Date? var mostRecentPumpDataDate: Date? - var loopStateInput = LoopAlgorithmInput( - predictionStart: Date(), + var loopStateInput = StoredDataAlgorithmInput( glucoseHistory: [], doses: [], carbEntries: [], + predictionStart: Date(), basal: [], sensitivity: [], carbRatio: [], @@ -866,13 +871,15 @@ fileprivate class MockBolusEntryViewModelDelegate: BolusEntryViewModelDelegate { suspendThreshold: nil, maxBolus: 3, maxBasalRate: 6, + useIntegralRetrospectiveCorrection: false, + includePositiveVelocityAndRC: true, carbAbsorptionModel: .piecewiseLinear, - recommendationInsulinType: .novolog, + recommendationInsulinModel: ExponentialInsulinModelPreset.rapidActingAdult, recommendationType: .manualBolus, automaticBolusApplicationFactor: 0.4 ) - func fetchData(for baseTime: Date, disablingPreMeal: Bool) async throws -> LoopAlgorithmInput { + func fetchData(for baseTime: Date, disablingPreMeal: Bool) async throws -> StoredDataAlgorithmInput { loopStateInput.predictionStart = baseTime return loopStateInput } @@ -925,14 +932,14 @@ fileprivate class MockBolusEntryViewModelDelegate: BolusEntryViewModelDelegate { var activeCarbs: CarbValue? var prediction: [PredictedGlucoseValue] = [] - var lastGeneratePredictionInput: LoopAlgorithmInput? + var lastGeneratePredictionInput: StoredDataAlgorithmInput? - func generatePrediction(input: LoopAlgorithmInput) throws -> [PredictedGlucoseValue] { + func generatePrediction(input: StoredDataAlgorithmInput) throws -> [PredictedGlucoseValue] { lastGeneratePredictionInput = input return prediction } - var algorithmOutput: LoopAlgorithmOutput = LoopAlgorithmOutput( + var algorithmOutput: AlgorithmOutput = AlgorithmOutput( recommendationResult: .success(.init()), predictedGlucose: [], effects: LoopAlgorithmEffects.emptyMock, diff --git a/LoopTests/ViewModels/ManualEntryDoseViewModelTests.swift b/LoopTests/ViewModels/ManualEntryDoseViewModelTests.swift index 46cb1e75a3..d21c3f9e43 100644 --- a/LoopTests/ViewModels/ManualEntryDoseViewModelTests.swift +++ b/LoopTests/ViewModels/ManualEntryDoseViewModelTests.swift @@ -10,6 +10,8 @@ import HealthKit import LoopCore import LoopKit import XCTest +import LoopAlgorithm + @testable import Loop @MainActor @@ -73,7 +75,7 @@ class ManualEntryDoseViewModelTests: XCTestCase { } fileprivate class MockManualEntryDoseViewModelDelegate: ManualDoseViewModelDelegate { - var pumpInsulinType: LoopKit.InsulinType? + var pumpInsulinType: InsulinType? var manualEntryBolusUnits: Double? var manualEntryDoseStartDate: Date? @@ -85,7 +87,7 @@ fileprivate class MockManualEntryDoseViewModelDelegate: ManualDoseViewModelDeleg manuallyEnteredDoseInsulinType = insulinType } - func insulinActivityDuration(for type: LoopKit.InsulinType?) -> TimeInterval { + func insulinActivityDuration(for type: InsulinType?) -> TimeInterval { return InsulinMath.defaultInsulinActivityDuration } @@ -95,5 +97,8 @@ fileprivate class MockManualEntryDoseViewModelDelegate: ManualDoseViewModelDeleg var scheduleOverride: TemporaryScheduleOverride? + func insulinModel(for type: LoopKit.InsulinType?) -> InsulinModel { + return ExponentialInsulinModelPreset.rapidActingAdult + } } diff --git a/LoopTests/ViewModels/SimpleBolusViewModelTests.swift b/LoopTests/ViewModels/SimpleBolusViewModelTests.swift index bc35213e9c..d2425abd0b 100644 --- a/LoopTests/ViewModels/SimpleBolusViewModelTests.swift +++ b/LoopTests/ViewModels/SimpleBolusViewModelTests.swift @@ -11,6 +11,7 @@ import HealthKit import LoopKit import LoopKitUI import LoopCore +import LoopAlgorithm @testable import Loop diff --git a/WatchApp Extension/ComplicationController.swift b/WatchApp Extension/ComplicationController.swift index 1eae019f17..0343a17d94 100644 --- a/WatchApp Extension/ComplicationController.swift +++ b/WatchApp Extension/ComplicationController.swift @@ -11,6 +11,7 @@ import WatchKit import LoopKit import LoopCore import os.log +import LoopAlgorithm final class ComplicationController: NSObject, CLKComplicationDataSource { diff --git a/WatchApp Extension/Controllers/CarbEntryListController.swift b/WatchApp Extension/Controllers/CarbEntryListController.swift index a704a942cd..8a2b74a420 100644 --- a/WatchApp Extension/Controllers/CarbEntryListController.swift +++ b/WatchApp Extension/Controllers/CarbEntryListController.swift @@ -10,6 +10,7 @@ import LoopCore import LoopKit import os.log import WatchKit +import LoopAlgorithm class CarbEntryListController: WKInterfaceController, IdentifiableClass { @IBOutlet private var table: WKInterfaceTable! @@ -79,7 +80,7 @@ extension CarbEntryListController { } private func reloadCarbEntries() { - let start = min(Calendar.current.startOfDay(for: Date()), Date(timeIntervalSinceNow: -loopManager.carbStore.maximumAbsorptionTimeInterval)) + let start = min(Calendar.current.startOfDay(for: Date()), Date(timeIntervalSinceNow: -CarbMath.maximumAbsorptionTimeInterval)) loopManager.carbStore.getCarbEntries(start: start) { (result) in switch result { diff --git a/WatchApp Extension/Controllers/ChartHUDController.swift b/WatchApp Extension/Controllers/ChartHUDController.swift index 341eece3f6..d093bca3c9 100644 --- a/WatchApp Extension/Controllers/ChartHUDController.swift +++ b/WatchApp Extension/Controllers/ChartHUDController.swift @@ -13,6 +13,7 @@ import HealthKit import SpriteKit import os.log import LoopCore +import LoopAlgorithm final class ChartHUDController: HUDInterfaceController, WKCrownDelegate { private enum TableRow: Int, CaseIterable { diff --git a/WatchApp Extension/Controllers/HUDInterfaceController.swift b/WatchApp Extension/Controllers/HUDInterfaceController.swift index 2ff5f54fb0..7ee49de7b7 100644 --- a/WatchApp Extension/Controllers/HUDInterfaceController.swift +++ b/WatchApp Extension/Controllers/HUDInterfaceController.swift @@ -9,6 +9,7 @@ import WatchKit import LoopCore import LoopKit +import LoopAlgorithm class HUDInterfaceController: WKInterfaceController { private var activeContextObserver: NSObjectProtocol? diff --git a/WatchApp Extension/Managers/ComplicationChartManager.swift b/WatchApp Extension/Managers/ComplicationChartManager.swift index bfca19ea24..1d71f66446 100644 --- a/WatchApp Extension/Managers/ComplicationChartManager.swift +++ b/WatchApp Extension/Managers/ComplicationChartManager.swift @@ -11,6 +11,7 @@ import UIKit import HealthKit import WatchKit import LoopKit +import LoopAlgorithm private let textInsets = UIEdgeInsets(top: 2, left: 2, bottom: 2, right: 2) diff --git a/WatchApp Extension/Managers/LoopDataManager.swift b/WatchApp Extension/Managers/LoopDataManager.swift index 1fcbdbd30c..1a0be226f2 100644 --- a/WatchApp Extension/Managers/LoopDataManager.swift +++ b/WatchApp Extension/Managers/LoopDataManager.swift @@ -12,6 +12,7 @@ import LoopKit import LoopCore import WatchConnectivity import os.log +import LoopAlgorithm class LoopDataManager { @@ -66,7 +67,6 @@ class LoopDataManager { carbStore = CarbStore( cacheStore: cacheStore, cacheLength: .hours(24), // Require 24 hours to store recent carbs "since midnight" for CarbEntryListController - defaultAbsorptionTimes: LoopCoreConstants.defaultCarbAbsorptionTimes, syncVersion: 0 ) glucoseStore = GlucoseStore( @@ -114,7 +114,7 @@ extension LoopDataManager { func requestCarbBackfill() { dispatchPrecondition(condition: .onQueue(.main)) - let start = min(Calendar.current.startOfDay(for: Date()), Date(timeIntervalSinceNow: -carbStore.maximumAbsorptionTimeInterval)) + let start = min(Calendar.current.startOfDay(for: Date()), Date(timeIntervalSinceNow: -CarbMath.maximumAbsorptionTimeInterval)) let userInfo = CarbBackfillRequestUserInfo(startDate: start) WCSession.default.sendCarbBackfillRequestMessage(userInfo) { (result) in switch result { diff --git a/WatchApp Extension/Models/GlucoseChartData.swift b/WatchApp Extension/Models/GlucoseChartData.swift index 4ed6bd7ee8..4bf9a2b2c8 100644 --- a/WatchApp Extension/Models/GlucoseChartData.swift +++ b/WatchApp Extension/Models/GlucoseChartData.swift @@ -9,6 +9,7 @@ import Foundation import HealthKit import LoopKit +import LoopAlgorithm struct GlucoseChartData { diff --git a/WatchApp Extension/Models/GlucoseChartScaler.swift b/WatchApp Extension/Models/GlucoseChartScaler.swift index cb03f8380b..953f5bf1ea 100644 --- a/WatchApp Extension/Models/GlucoseChartScaler.swift +++ b/WatchApp Extension/Models/GlucoseChartScaler.swift @@ -11,6 +11,7 @@ import CoreGraphics import HealthKit import LoopKit import WatchKit +import LoopAlgorithm enum CoordinateSystem { diff --git a/WatchApp Extension/Scenes/GlucoseChartValueHashable.swift b/WatchApp Extension/Scenes/GlucoseChartValueHashable.swift index 4737a2336f..5060e5d372 100644 --- a/WatchApp Extension/Scenes/GlucoseChartValueHashable.swift +++ b/WatchApp Extension/Scenes/GlucoseChartValueHashable.swift @@ -7,6 +7,7 @@ import LoopKit import HealthKit +import LoopAlgorithm protocol GlucoseChartValueHashable {