diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 299c19910d..5230c2ce5e 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -7,6 +7,42 @@ objects = { /* Begin PBXBuildFile section */ + 52CB9A826E3B3DFC4E483084 /* AlgorithmBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36421A4B9676EF68CDDC4ECD /* AlgorithmBridge.swift */; }; + 01F99A06F17502CC60E9535C /* AlgorithmBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36421A4B9676EF68CDDC4ECD /* AlgorithmBridge.swift */; }; + 527427E1C81FAB157FEEA18B /* BolusDosingDecision.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FB75F0252BE320004C7D3F /* BolusDosingDecision.swift */; }; + B2F374A7AC2458B5AC5CE371 /* BolusDosingDecision.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FB75F0252BE320004C7D3F /* BolusDosingDecision.swift */; }; + 3EC6DF601CC5B95DD71BB52D /* TempBasalRecommendation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F2CAA92B76B3EE00D7F581 /* TempBasalRecommendation.swift */; }; + CEEBC321F91A87AFC45E1015 /* TempBasalRecommendation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F2CAA92B76B3EE00D7F581 /* TempBasalRecommendation.swift */; }; + DEAABF073E29356AF4D5CAEB /* SimpleInsulinDose.swift in Sources */ = {isa = PBXBuildFile; fileRef = C16F511A2B89363A00EFD7A1 /* SimpleInsulinDose.swift */; }; + 0921C6BF71476F50FAC0C7F2 /* SimpleInsulinDose.swift in Sources */ = {isa = PBXBuildFile; fileRef = C16F511A2B89363A00EFD7A1 /* SimpleInsulinDose.swift */; }; + 46CCF5F0CA2280F458F65B80 /* StoredDataAlgorithmInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = C16F51182B891DB600EFD7A1 /* StoredDataAlgorithmInput.swift */; }; + 81A328004D241282EA326E69 /* StoredDataAlgorithmInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = C16F51182B891DB600EFD7A1 /* StoredDataAlgorithmInput.swift */; }; + 187E5B111D2877B585483489 /* SettingsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00A28F30399B460CABF4EDD2 /* SettingsProvider.swift */; }; + A3BAB5421752C7A8E1E70D7F /* SettingsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00A28F30399B460CABF4EDD2 /* SettingsProvider.swift */; }; + 6AE03D0437B61A334F2FE2E4 /* PumpManagerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A999D40524663D18004C89D4 /* PumpManagerError.swift */; }; + 36457634F78A15F4CCFB2A7C /* PumpManagerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A999D40524663D18004C89D4 /* PumpManagerError.swift */; }; + CF8C0C366A2EB7DBF96BA005 /* DosingDecisionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B996F127238705002DC09C /* DosingDecisionStore.swift */; }; + 77E1CC783E13DB6571487416 /* DosingDecisionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B996F127238705002DC09C /* DosingDecisionStore.swift */; }; + EAF0BAC73C927B8C75B5CEE7 /* LoopWarning.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B996EF27235191002DC09C /* LoopWarning.swift */; }; + 1EF7ABFF44F28418B9081884 /* LoopWarning.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B996EF27235191002DC09C /* LoopWarning.swift */; }; + 4AA88667600C539BA7556DC6 /* LoopError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 436A0DA41D236A2A00104B24 /* LoopError.swift */; }; + D5321AAC567B470277F4693C /* LoopError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 436A0DA41D236A2A00104B24 /* LoopError.swift */; }; + A11291908633A17B3F06CE29 /* LoopConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1C660D0252E4DD5009B5C32 /* LoopConstants.swift */; }; + CA7D019306B2674E4BAF6DE3 /* LoopConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1C660D0252E4DD5009B5C32 /* LoopConstants.swift */; }; + 8C6801E4F5D9D5A8F614FB1F /* CarbEntryChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8378B7D1F532A5F47CEBB6AE /* CarbEntryChange.swift */; }; + 59A9DDD71F0CB765C3529F9C /* CarbEntryChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8378B7D1F532A5F47CEBB6AE /* CarbEntryChange.swift */; }; + 1FFB250EDF89EBC360A7A5C1 /* GlucoseStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95D380424EADF78005E2F50 /* GlucoseStoreProtocol.swift */; }; + 08B8082DACB7E55849F34C32 /* GlucoseStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95D380424EADF78005E2F50 /* GlucoseStoreProtocol.swift */; }; + E0F0D982F2ACD9D78617BA0E /* DosingDecisionStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E98A55EE24EDD6E60008715D /* DosingDecisionStoreProtocol.swift */; }; + 931E35F788B8CF54BC5277C8 /* DosingDecisionStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E98A55EE24EDD6E60008715D /* DosingDecisionStoreProtocol.swift */; }; + 451FFEF51B975CE332CC6C6D /* DoseStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95D380024EADE7C005E2F50 /* DoseStoreProtocol.swift */; }; + 690482C3E6D6A94D7BA232B6 /* DoseStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95D380024EADE7C005E2F50 /* DoseStoreProtocol.swift */; }; + 0390983B63120E47B543264F /* CarbStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95D380224EADF36005E2F50 /* CarbStoreProtocol.swift */; }; + 3CD16E39CAB99674C3AB351C /* CarbStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95D380224EADF36005E2F50 /* CarbStoreProtocol.swift */; }; + D96DC2868F8E7ADEEA335047 /* LoopAlgorithmSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 812023C7C7DB7C71CD750B62 /* LoopAlgorithmSupport.swift */; }; + 104021B7449DA59717EA4E5D /* LoopAlgorithmSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 812023C7C7DB7C71CD750B62 /* LoopAlgorithmSupport.swift */; }; + 229FEACAEAB2DF60B3DB5C84 /* LoopCycle.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB3DFFEB9BB163D1B87B3563 /* LoopCycle.swift */; }; + D23A31DF90992466C4626B34 /* LoopCycle.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB3DFFEB9BB163D1B87B3563 /* LoopCycle.swift */; }; 01414522FC033F2B7A9B223A /* Loop/Resources/Sounds/crying.caf in Resources */ = {isa = PBXBuildFile; fileRef = 85203550670F2E9BE92F27D8 /* Loop/Resources/Sounds/crying.caf */; }; 0A02E77AD7536C322933D49B /* Loop/Resources/Sounds/minimal.caf in Resources */ = {isa = PBXBuildFile; fileRef = 74145BE3C46270883D295A1F /* Loop/Resources/Sounds/minimal.caf */; }; 124B300527D84BE2A1233A91 /* Loop/Resources/Sounds/urgent_low.caf in Resources */ = {isa = PBXBuildFile; fileRef = C9EA3F48338B49BD9C6BD0F9 /* Loop/Resources/Sounds/urgent_low.caf */; }; @@ -66,8 +102,6 @@ 147EFE8E2A8BCC5500272438 /* DefaultAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 147EFE8D2A8BCC5500272438 /* DefaultAssets.xcassets */; }; 147EFE902A8BCD8000272438 /* DerivedAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 147EFE8F2A8BCD8000272438 /* DerivedAssets.xcassets */; }; 147EFE922A8BCD8A00272438 /* DerivedAssetsBase.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 147EFE912A8BCD8A00272438 /* DerivedAssetsBase.xcassets */; }; - 1F0000012FBA000100000001 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 1F0000002FBA000100000001 /* AppIcon.icon */; }; - 1F0000022FBA000100000001 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 1F0000002FBA000100000001 /* AppIcon.icon */; }; 1481F9BB28DA26F4004C5AEB /* LoopUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F75288B1DFE1DC600C322D6 /* LoopUI.framework */; }; 149A28BB2A853E5100052EDF /* CarbEntryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149A28BA2A853E5100052EDF /* CarbEntryViewModel.swift */; }; 149A28BD2A853E6C00052EDF /* CarbEntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149A28BC2A853E6C00052EDF /* CarbEntryView.swift */; }; @@ -129,6 +163,8 @@ 1DDE273E24AEA4B000796622 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE09BA824A3E23F009EE9F9 /* SettingsView.swift */; }; 1DDE274024AEA4F200796622 /* NotificationsCriticalAlertPermissionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA46B5F2492E2E300D71A63 /* NotificationsCriticalAlertPermissionsView.swift */; }; 1DFE9E172447B6270082C280 /* UserNotificationAlertSchedulerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFE9E162447B6270082C280 /* UserNotificationAlertSchedulerTests.swift */; }; + 1F0000012FBA000100000001 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 1F0000002FBA000100000001 /* AppIcon.icon */; }; + 1F0000022FBA000100000001 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 1F0000002FBA000100000001 /* AppIcon.icon */; }; 230BE095719B8D8969B32A9C /* Loop/Resources/Sounds/bloop.caf in Resources */ = {isa = PBXBuildFile; fileRef = AF16A5EDEB9DF756890E0C09 /* Loop/Resources/Sounds/bloop.caf */; }; 2C694BFD5806E573AFAB5E2E /* Loop/Resources/Sounds/dings.caf in Resources */ = {isa = PBXBuildFile; fileRef = 62F7CB5D5DC337E705032AC7 /* Loop/Resources/Sounds/dings.caf */; }; 33644743F20B3706E3107FAC /* Loop/Resources/Sounds/mood_synth.caf in Resources */ = {isa = PBXBuildFile; fileRef = BEC0BA802BE613F144AC3A70 /* Loop/Resources/Sounds/mood_synth.caf */; }; @@ -171,7 +207,6 @@ 4345E40221F67300009E00E5 /* GetBolusRecommendationUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43DE92581C5479E4001FFDE1 /* GetBolusRecommendationUserInfo.swift */; }; 434FF1EE1CF27EEF000DB779 /* UITableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 434FF1ED1CF27EEF000DB779 /* UITableViewCell.swift */; }; 43517917230A0E1A0072ECC0 /* WKInterfaceLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43517916230A0E1A0072ECC0 /* WKInterfaceLabel.swift */; }; - 436A0DA51D236A2A00104B24 /* LoopError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 436A0DA41D236A2A00104B24 /* LoopError.swift */; }; 4372E487213C86240068E043 /* SampleValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4372E486213C86240068E043 /* SampleValue.swift */; }; 4372E488213C862B0068E043 /* SampleValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4372E486213C86240068E043 /* SampleValue.swift */; }; 4372E48B213CB5F00068E043 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4372E48A213CB5F00068E043 /* Double.swift */; }; @@ -274,8 +309,6 @@ 4FDDD23720DC51DF00D04B16 /* LoopDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FDDD23620DC51DF00D04B16 /* LoopDataManager.swift */; }; D7A3000100000000000A0002 /* WatchCGMHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A3000100000000000A0001 /* WatchCGMHost.swift */; }; D7A3000100000000000A0004 /* WatchDeviceManagers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A3000100000000000A0003 /* WatchDeviceManagers.swift */; }; - 101AA7370B0E499FBD88BB9F /* G7SensorKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = F8760AF31E3C43F6AFBA1BE7 /* G7SensorKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 921B7FAE002E40EFAB3F50C6 /* G7SensorKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F8760AF31E3C43F6AFBA1BE7 /* G7SensorKit.framework */; }; 8B5CF0144D79477AACBE4A82 /* LoopKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4344628320A7A3BE00C4BE6F /* LoopKit.framework */; }; 4FF4D0F81E1725B000846527 /* NibLoadable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 434F54561D287FDB002A9274 /* NibLoadable.swift */; }; 5021CA8C264146C9DFF33E9D /* PluginPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE102E23D8C1C1291EF66387 /* PluginPickerView.swift */; }; @@ -377,6 +410,9 @@ 89F9119424358E4500ECCAF3 /* CarbAbsorptionTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89F9119324358E4500ECCAF3 /* CarbAbsorptionTime.swift */; }; 89F9119624358E6900ECCAF3 /* BolusPickerValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89F9119524358E6900ECCAF3 /* BolusPickerValues.swift */; }; 89FE21AD24AC57E30033F501 /* Collection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89FE21AC24AC57E30033F501 /* Collection.swift */; }; + 8B5CF0144D79477AACBE4A82 /* LoopKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4344628320A7A3BE00C4BE6F /* LoopKit.framework */; }; + 992345AB303643A60064B448 /* BuildDetails.plist in Resources */ = {isa = PBXBuildFile; fileRef = C1EE9E802A38D0FB0064784A /* BuildDetails.plist */; }; + 992345AC303643A60064B448 /* BuildDetails.swift in Resources */ = {isa = PBXBuildFile; fileRef = C110888C2A3913C600BA4898 /* BuildDetails.swift */; }; 9960C6964DD51D81093FAD5D /* Loop/Resources/Sounds/chime.caf in Resources */ = {isa = PBXBuildFile; fileRef = 3F7A1D72697082DC3A14587E /* Loop/Resources/Sounds/chime.caf */; }; 9B8FCBDD6E3A9E603C4AABAD /* Loop/Resources/Sounds/bright_alarm.caf in Resources */ = {isa = PBXBuildFile; fileRef = 270F994CD56D566028B3CB25 /* Loop/Resources/Sounds/bright_alarm.caf */; }; A1B2C3D40000000000000002 /* GlucoseAlertManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D40000000000000001 /* GlucoseAlertManagerTests.swift */; }; @@ -401,13 +437,10 @@ A97F250825E056D500F0EE19 /* OnboardingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97F250725E056D500F0EE19 /* OnboardingManager.swift */; }; A98556852493F901000FD662 /* AlertStore+SimulatedCoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98556842493F901000FD662 /* AlertStore+SimulatedCoreData.swift */; }; A987CD4924A58A0100439ADC /* ZipArchive.swift in Sources */ = {isa = PBXBuildFile; fileRef = A987CD4824A58A0100439ADC /* ZipArchive.swift */; }; - A999D40624663D18004C89D4 /* PumpManagerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A999D40524663D18004C89D4 /* PumpManagerError.swift */; }; A9A056B324B93C62007CF06D /* CriticalEventLogExportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A056B224B93C62007CF06D /* CriticalEventLogExportView.swift */; }; A9A056B524B94123007CF06D /* CriticalEventLogExportViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A056B424B94123007CF06D /* CriticalEventLogExportViewModel.swift */; }; A9A63F8E246B271600588D5B /* NSTimeInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439897341CD2F7DE00223065 /* NSTimeInterval.swift */; }; A9B607B0247F000F00792BE4 /* UserNotifications+Loop.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B607AF247F000F00792BE4 /* UserNotifications+Loop.swift */; }; - A9B996F027235191002DC09C /* LoopWarning.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B996EF27235191002DC09C /* LoopWarning.swift */; }; - A9B996F227238705002DC09C /* DosingDecisionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B996F127238705002DC09C /* DosingDecisionStore.swift */; }; A9BD28E7272226B40071DF15 /* TestLocalizedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BD28E6272226B40071DF15 /* TestLocalizedError.swift */; }; A9C1719725366F780053BCBD /* WatchHistoricalGlucoseTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C1719625366F780053BCBD /* WatchHistoricalGlucoseTest.swift */; }; A9C62D842331700E00535612 /* DiagnosticLog+Subsystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C62D832331700D00535612 /* DiagnosticLog+Subsystem.swift */; }; @@ -430,8 +463,6 @@ A9F703732489BC8500C98AD8 /* CarbStore+SimulatedCoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F703722489BC8500C98AD8 /* CarbStore+SimulatedCoreData.swift */; }; A9F703752489C9A000C98AD8 /* GlucoseStore+SimulatedCoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F703742489C9A000C98AD8 /* GlucoseStore+SimulatedCoreData.swift */; }; A9F703772489D8AA00C98AD8 /* PersistentDeviceLog+SimulatedCoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F703762489D8AA00C98AD8 /* PersistentDeviceLog+SimulatedCoreData.swift */; }; - A9FB75F1252BE320004C7D3F /* BolusDosingDecision.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FB75F0252BE320004C7D3F /* BolusDosingDecision.swift */; }; - 2437A340C1560EEDE55E3F7B /* CarbEntryChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8378B7D1F532A5F47CEBB6AE /* CarbEntryChange.swift */; }; B4001CEE28CBBC82002FB414 /* AlertManagementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4001CED28CBBC82002FB414 /* AlertManagementView.swift */; }; A1E47000000000000000A001 /* AlertHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1E47000000000000000A002 /* AlertHistoryView.swift */; }; B405E35A24D2B1A400DD058D /* HUDAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4F2C15961E09E94E00E160D4 /* HUDAssets.xcassets */; }; @@ -533,7 +564,6 @@ 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 */; }; 52ECDA0F09B4C8F23A4B450E /* InsulinDeliveryHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C16D18C8806FF48331134C /* InsulinDeliveryHistory.swift */; }; 8879F70ABCDF1E10619547A4 /* SetupChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12CD655083D4816F87B13463 /* SetupChange.swift */; }; 7F8C56209DE2227C701527B9 /* OutcomesPackage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22F7AB4EB9F6CCDF1593CB2E /* OutcomesPackage.swift */; }; @@ -547,7 +577,6 @@ 6D16FF26E7F84F97B5DB6A88 /* CurrentSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFD4A26A23202AF2EA506FCF /* CurrentSetupView.swift */; }; D70F014FFB67BA58678452E9 /* SetupChangeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03EC166B8AD05800AFCD474E /* SetupChangeTests.swift */; }; 5B0E7D2A91C34F68A2D1E401 /* OutcomesPackageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C1F8E3BA2D45079B3E2F512 /* OutcomesPackageTests.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 */; }; @@ -588,7 +617,6 @@ C1AF062329426300002C1B19 /* ManualGlucoseEntryRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1AF062229426300002C1B19 /* ManualGlucoseEntryRow.swift */; }; C1B80D632AF97D7200AB7705 /* LoopDataManager+CarbAbsorption.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B80D622AF97D7200AB7705 /* LoopDataManager+CarbAbsorption.swift */; }; C1BE2ADD2FCDE9F700D5A369 /* LoopKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C1BE2ADC2FCDE9F700D5A369 /* LoopKit.framework */; }; - C1C660D1252E4DD5009B5C32 /* LoopConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1C660D0252E4DD5009B5C32 /* LoopConstants.swift */; }; C1CCF1122858FA900035389C /* LoopCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43D9FFCF21EAE05D00AF44BF /* LoopCore.framework */; }; C1D0B6302986D4D90098D215 /* LocalizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D0B62F2986D4D90098D215 /* LocalizedString.swift */; }; C1D0B6312986D4D90098D215 /* LocalizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D0B62F2986D4D90098D215 /* LocalizedString.swift */; }; @@ -644,7 +672,6 @@ 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 */; }; @@ -659,6 +686,7 @@ CB1AF8155B1761A30ACA5591 /* Loop/Resources/Sounds/synth.caf in Resources */ = {isa = PBXBuildFile; fileRef = 83081FFB0497A4596F597830 /* Loop/Resources/Sounds/synth.caf */; }; D6E28676C2C7751C77C7F2FB /* Loop/Resources/Sounds/spring.caf in Resources */ = {isa = PBXBuildFile; fileRef = 9471E156FC10D6D0FF219C77 /* Loop/Resources/Sounds/spring.caf */; }; DD3DBD292A33AFE9000F8B5B /* IntegralRetrospectiveCorrectionSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3DBD282A33AFE9000F8B5B /* IntegralRetrospectiveCorrectionSelectionView.swift */; }; + DDBD0002000000000000BD02 /* BuildDetails.plist in Resources */ = {isa = PBXBuildFile; fileRef = C1EE9E802A38D0FB0064784A /* BuildDetails.plist */; }; A1B2C3D42E8F000100000002 /* GuardAndRunwaySelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D42E8F000100000001 /* GuardAndRunwaySelectionView.swift */; }; 883675B93130EED04D819D0A /* SlowRetrospectiveCorrectionSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1622757183D5C361AFB3FB5C /* SlowRetrospectiveCorrectionSelectionView.swift */; }; DDC389FC2A2BC6670066E2E8 /* SettingsView+algorithmExperimentsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC389FB2A2BC6670066E2E8 /* SettingsView+algorithmExperimentsSection.swift */; }; @@ -673,11 +701,7 @@ E942DE96253BE68F00AC532D /* NSBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 430DA58D1D4AEC230097D1CA /* NSBundle.swift */; }; E942DE9F253BE6A900AC532D /* NSTimeInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439897341CD2F7DE00223065 /* NSTimeInterval.swift */; }; E942DF34253BF87F00AC532D /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 43785E9B2120E7060057DED1 /* Intents.intentdefinition */; }; - E95D380124EADE7C005E2F50 /* DoseStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95D380024EADE7C005E2F50 /* DoseStoreProtocol.swift */; }; - E95D380324EADF36005E2F50 /* CarbStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95D380224EADF36005E2F50 /* CarbStoreProtocol.swift */; }; - E95D380524EADF78005E2F50 /* GlucoseStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95D380424EADF78005E2F50 /* GlucoseStoreProtocol.swift */; }; E98A55ED24EDD6380008715D /* LatestStoredSettingsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E98A55EC24EDD6380008715D /* LatestStoredSettingsProvider.swift */; }; - E98A55EF24EDD6E60008715D /* DosingDecisionStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E98A55EE24EDD6E60008715D /* DosingDecisionStoreProtocol.swift */; }; E98A55F124EDD85E0008715D /* MockDosingDecisionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E98A55F024EDD85E0008715D /* MockDosingDecisionStore.swift */; }; E98A55F324EDD9530008715D /* MockSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E98A55F224EDD9530008715D /* MockSettingsStore.swift */; }; E9B07F7F253BBA6500BAD8F8 /* IntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9B07F7E253BBA6500BAD8F8 /* IntentHandler.swift */; }; @@ -722,7 +746,6 @@ 1C99EE4A3BB181BB23DEF066 /* LoopAlgorithm in Frameworks */ = {isa = PBXBuildFile; productRef = C42F6B8A2B1B9E0142078BF9 /* LoopAlgorithm */; }; BD37CA2F10445DB600A35199 /* TemporaryScheduleOverrideAlgorithmTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C372374D3D3D9C5F8E3AD93 /* TemporaryScheduleOverrideAlgorithmTests.swift */; }; 5E77F6516D69B22A57EB850A /* LoopAlgorithm in Frameworks */ = {isa = PBXBuildFile; productRef = 761F6A285BE2833DF06F7E55 /* LoopAlgorithm */; }; - 613BF2B78104AB9694590C65 /* AlgorithmBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36421A4B9676EF68CDDC4ECD /* AlgorithmBridge.swift */; }; 767B36174D190F3191F2EA73 /* LoopKitTypeAliases.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35FAEFD820F50BFF211AC6D9 /* LoopKitTypeAliases.swift */; }; 8DC1CE32F39301122B550325 /* LoopKitTypeAliases.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35FAEFD820F50BFF211AC6D9 /* LoopKitTypeAliases.swift */; }; ABD9218EB98C9216FBB04382 /* LoopKitTypeAliases.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35FAEFD820F50BFF211AC6D9 /* LoopKitTypeAliases.swift */; }; @@ -832,7 +855,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 101AA7370B0E499FBD88BB9F /* G7SensorKit.framework in Embed Frameworks */, 43C05CB221EBD88A006FB252 /* LoopCore.framework in Embed Frameworks */, C1275E1C2E82269A0013B99D /* LoopKit.framework in Embed Frameworks */, ); @@ -865,6 +887,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 00A28F30399B460CABF4EDD2 /* SettingsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsProvider.swift; sourceTree = ""; }; 0FC34B523FE5A50C920506D5 /* Loop/Resources/Sounds/alarm.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/alarm.caf; sourceTree = SOURCE_ROOT; }; 142CB7582A60BF2E0075748A /* EditMode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditMode.swift; sourceTree = ""; }; 142CB75A2A60BFC30075748A /* FavoriteFoodsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FavoriteFoodsView.swift; sourceTree = ""; }; @@ -878,7 +901,6 @@ 147EFE8D2A8BCC5500272438 /* DefaultAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = DefaultAssets.xcassets; sourceTree = ""; }; 147EFE8F2A8BCD8000272438 /* DerivedAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = DerivedAssets.xcassets; sourceTree = ""; }; 147EFE912A8BCD8A00272438 /* DerivedAssetsBase.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = DerivedAssetsBase.xcassets; sourceTree = ""; }; - 1F0000002FBA000100000001 /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIcon.icon; sourceTree = ""; }; 149A28BA2A853E5100052EDF /* CarbEntryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbEntryViewModel.swift; sourceTree = ""; }; 149A28BC2A853E6C00052EDF /* CarbEntryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbEntryView.swift; sourceTree = ""; }; 149A28E32A8A63A700052EDF /* FavoriteFoodDetailView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FavoriteFoodDetailView.swift; sourceTree = ""; }; @@ -927,8 +949,10 @@ 1DC63E7325351BDF004605DA /* TrueTime.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = TrueTime.framework; path = Carthage/Build/iOS/TrueTime.framework; sourceTree = ""; }; 1DE09BA824A3E23F009EE9F9 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 1DFE9E162447B6270082C280 /* UserNotificationAlertSchedulerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationAlertSchedulerTests.swift; sourceTree = ""; }; + 1F0000002FBA000100000001 /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIcon.icon; sourceTree = ""; }; 21F13EB666EBFDD03F99CB7E /* Loop/Resources/Sounds/trill.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/trill.caf; sourceTree = SOURCE_ROOT; }; 270F994CD56D566028B3CB25 /* Loop/Resources/Sounds/bright_alarm.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/bright_alarm.caf; sourceTree = SOURCE_ROOT; }; + C16F51182B891DB600EFD7A1 /* StoredDataAlgorithmInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredDataAlgorithmInput.swift; sourceTree = ""; }; 3ED319862EB659E600820BCF /* BasalViewActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasalViewActivity.swift; sourceTree = ""; }; 3ED319872EB659E600820BCF /* ChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartView.swift; sourceTree = ""; }; 3ED319882EB659E600820BCF /* GlucoseLiveActivityConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseLiveActivityConfiguration.swift; sourceTree = ""; }; @@ -943,6 +967,7 @@ 3ED319A02EB65B4100820BCF /* Bootstrap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bootstrap.swift; sourceTree = ""; }; 3ED319A22EB65DA300820BCF /* LiveActivityManagerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityManagerProxy.swift; sourceTree = ""; }; 3F7A1D72697082DC3A14587E /* Loop/Resources/Sounds/chime.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/chime.caf; sourceTree = SOURCE_ROOT; }; + 42BE0808089545E7BE6AE359 /* NSBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = NSBundle.swift; path = Common/Extensions/NSBundle.swift; sourceTree = SOURCE_ROOT; }; 4302F4E01D4E9C8900F0FCAF /* TextFieldTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldTableViewController.swift; sourceTree = ""; }; 4302F4E21D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InsulinDeliveryTableViewController.swift; sourceTree = ""; }; 430B29892041F54A00BA9F93 /* NSUserDefaults.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSUserDefaults.swift; sourceTree = ""; }; @@ -1077,6 +1102,7 @@ 43FCEEA8221A615B0013DD30 /* StatusChartsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusChartsManager.swift; sourceTree = ""; }; 43FCEEAC221A66780013DD30 /* DateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormatter.swift; sourceTree = ""; }; 491F8F0D89972E9D8903CFA7 /* Loop/Resources/Sounds/high_chimes.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/high_chimes.caf; sourceTree = SOURCE_ROOT; }; + C16F511A2B89363A00EFD7A1 /* SimpleInsulinDose.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleInsulinDose.swift; sourceTree = ""; }; 4A645E0E2DDA534A635C9201 /* Loop/Resources/Sounds/clear_chimes.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/clear_chimes.caf; sourceTree = SOURCE_ROOT; }; 4D3B40021D4A9DFE00BC6334 /* G4ShareSpy.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = G4ShareSpy.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4F08DE8E1E7BB871006741EA /* CollectionType+Loop.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CollectionType+Loop.swift"; sourceTree = ""; }; @@ -1102,13 +1128,13 @@ 4FDDD23620DC51DF00D04B16 /* LoopDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopDataManager.swift; sourceTree = ""; }; D7A3000100000000000A0001 /* WatchCGMHost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchCGMHost.swift; sourceTree = ""; }; D7A3000100000000000A0003 /* WatchDeviceManagers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchDeviceManagers.swift; sourceTree = ""; }; - F8760AF31E3C43F6AFBA1BE7 /* G7SensorKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = G7SensorKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4FF4D0FF1E18374700846527 /* WatchContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchContext.swift; sourceTree = ""; }; 5181B60CBF3CB2DA537F0475 /* Loop/Resources/Sounds/simple.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/simple.caf; sourceTree = SOURCE_ROOT; }; 62F7CB5D5DC337E705032AC7 /* Loop/Resources/Sounds/dings.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/dings.caf; sourceTree = SOURCE_ROOT; }; 6D5FCC7207E50ACC682E9E6E /* Loop/Resources/Sounds/honk.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/honk.caf; sourceTree = SOURCE_ROOT; }; 74145BE3C46270883D295A1F /* Loop/Resources/Sounds/minimal.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/minimal.caf; sourceTree = SOURCE_ROOT; }; 7D23667C21250C7E0028B67D /* LocalizedString.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LocalizedString.swift; path = LoopUI/Common/LocalizedString.swift; sourceTree = SOURCE_ROOT; }; + C1F2CAA92B76B3EE00D7F581 /* TempBasalRecommendation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempBasalRecommendation.swift; sourceTree = ""; }; 7D9BEF132335EC4B005DCFD6 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Intents.strings; sourceTree = ""; }; 7D9BEF292335EC58005DCFD6 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Intents.strings"; sourceTree = ""; }; 7D9BEF3F2335EC62005DCFD6 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Intents.strings; sourceTree = ""; }; @@ -1241,6 +1267,7 @@ A96DAC232838325900D94E38 /* DiagnosticLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiagnosticLog.swift; sourceTree = ""; }; A96DAC292838EF8A00D94E38 /* DiagnosticLogTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiagnosticLogTests.swift; sourceTree = ""; }; A96DAC2B2838F31200D94E38 /* SharedLogging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedLogging.swift; sourceTree = ""; }; + A973A0F6CF644FFAA48989FC /* UserDefaults+LoopIntents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "UserDefaults+LoopIntents.swift"; path = "Common/Extensions/UserDefaults+LoopIntents.swift"; sourceTree = SOURCE_ROOT; }; A977A2F324ACFECF0059C207 /* CriticalEventLogExportManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CriticalEventLogExportManager.swift; sourceTree = ""; }; A97F250725E056D500F0EE19 /* OnboardingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingManager.swift; sourceTree = ""; }; A98556842493F901000FD662 /* AlertStore+SimulatedCoreData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlertStore+SimulatedCoreData.swift"; sourceTree = ""; }; @@ -1396,9 +1423,10 @@ C16575722538AFF6004AE16E /* CGMStalenessMonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGMStalenessMonitorTests.swift; sourceTree = ""; }; C16575742539FD60004AE16E /* LoopCoreConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopCoreConstants.swift; sourceTree = ""; }; C16B983D26B4893300256B05 /* DoseEnactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DoseEnactor.swift; sourceTree = ""; }; + 812023C7C7DB7C71CD750B62 /* LoopAlgorithmSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LoopAlgorithmSupport.swift"; sourceTree = ""; }; + FB3DFFEB9BB163D1B87B3563 /* LoopCycle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LoopCycle.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 = ""; }; C4C16D18C8806FF48331134C /* InsulinDeliveryHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsulinDeliveryHistory.swift; sourceTree = ""; }; 12CD655083D4816F87B13463 /* SetupChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupChange.swift; sourceTree = ""; }; 22F7AB4EB9F6CCDF1593CB2E /* OutcomesPackage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutcomesPackage.swift; sourceTree = ""; }; @@ -1412,7 +1440,6 @@ CFD4A26A23202AF2EA506FCF /* CurrentSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentSetupView.swift; sourceTree = ""; }; 03EC166B8AD05800AFCD474E /* SetupChangeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupChangeTests.swift; sourceTree = ""; }; 6C1F8E3BA2D45079B3E2F512 /* OutcomesPackageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutcomesPackageTests.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 = ""; }; @@ -1481,7 +1508,6 @@ 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 = ""; }; C1F7822527CC056900C0919A /* SettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsManager.swift; sourceTree = ""; }; @@ -1617,7 +1643,10 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D96C7D3C9C4B414AAEAB3339 /* RileyLinkBLEKit.framework in Frameworks */, + D6A12EF6F5AD4B4CAABDD49B /* RileyLinkKit.framework in Frameworks */, 921B7FAE002E40EFAB3F50C6 /* G7SensorKit.framework in Frameworks */, + C58C1A2FC9604165AF3A144E /* OmnipodKit.framework in Frameworks */, 8B5CF0144D79477AACBE4A82 /* LoopKit.framework in Frameworks */, C1E2773E224177C000354103 /* ClockKit.framework in Frameworks */, 4344628220A7A37F00C4BE6F /* CoreBluetooth.framework in Frameworks */, @@ -1692,6 +1721,38 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 1CFB4FFCC843C243237519A0 /* Extensions */ = { + isa = PBXGroup; + children = ( + C1F2CAA92B76B3EE00D7F581 /* TempBasalRecommendation.swift */, + A999D40524663D18004C89D4 /* PumpManagerError.swift */, + A9B996F127238705002DC09C /* DosingDecisionStore.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 5F0AED2F82294E85F65DE0B9 /* Store Protocols */ = { + isa = PBXGroup; + children = ( + E95D380424EADF78005E2F50 /* GlucoseStoreProtocol.swift */, + E98A55EE24EDD6E60008715D /* DosingDecisionStoreProtocol.swift */, + E95D380024EADE7C005E2F50 /* DoseStoreProtocol.swift */, + E95D380224EADF36005E2F50 /* CarbStoreProtocol.swift */, + ); + path = "Store Protocols"; + sourceTree = ""; + }; + 982246E71B12B5C493AD5140 /* Managers */ = { + isa = PBXGroup; + children = ( + 00A28F30399B460CABF4EDD2 /* SettingsProvider.swift */, + 812023C7C7DB7C71CD750B62 /* LoopAlgorithmSupport.swift */, + FB3DFFEB9BB163D1B87B3563 /* LoopCycle.swift */, + 5F0AED2F82294E85F65DE0B9 /* Store Protocols */, + ); + path = Managers; + sourceTree = ""; + }; 14B1736128AED9EC006CCD7C /* Loop Widget Extension */ = { isa = PBXGroup; children = ( @@ -1846,16 +1907,11 @@ isa = PBXGroup; children = ( FEEDF0000000000000000000 /* GlucoseStatistics.swift */, - A9FB75F0252BE320004C7D3F /* BolusDosingDecision.swift */, - 8378B7D1F532A5F47CEBB6AE /* CarbEntryChange.swift */, C1EF747128D6A44A00C8C083 /* CrashRecoveryManager.swift */, 1455ACB12C667BEE004F44F2 /* Deeplink.swift */, B40D07C6251A89D500C1C6D7 /* GlucoseDisplay.swift */, 43C2FAE01EB656A500364AFF /* GlucoseEffectVelocity.swift */, - C1C660D0252E4DD5009B5C32 /* LoopConstants.swift */, - 436A0DA41D236A2A00104B24 /* LoopError.swift */, E9C00EF424C623EF00628F35 /* LoopSettings+Loop.swift */, - A9B996EF27235191002DC09C /* LoopWarning.swift */, C17824A41E1AD4D100D9D25C /* ManualBolusRecommendation.swift */, 4F526D601DF8D9A900A04910 /* NetBasal.swift */, 438D42F81D7C88BC003244B0 /* PredictionInputEffect.swift */, @@ -1863,16 +1919,13 @@ C1E3862428247B7100F561A4 /* StoredLoopNotRunningNotification.swift */, 4328E0311CFC068900E199AA /* WatchContext+LoopKit.swift */, A987CD4824A58A0100439ADC /* ZipArchive.swift */, - C16F51182B891DB600EFD7A1 /* StoredDataAlgorithmInput.swift */, C4C16D18C8806FF48331134C /* InsulinDeliveryHistory.swift */, 12CD655083D4816F87B13463 /* SetupChange.swift */, 22F7AB4EB9F6CCDF1593CB2E /* OutcomesPackage.swift */, 275032AC3D26F4EC5B6DEA15 /* OutcomesShareClient.swift */, D5E7CDB17CCE70885EE93850 /* SetupComparison.swift */, - C16F511A2B89363A00EFD7A1 /* SimpleInsulinDose.swift */, C152B9F42C9C7D4A00ACBC06 /* AutomationHistoryEntry.swift */, 84213C882D94941000642E78 /* InsulinDeliveryLogEvent.swift */, - 36421A4B9676EF68CDDC4ECD /* AlgorithmBridge.swift */, ); path = Models; sourceTree = ""; @@ -2072,6 +2125,8 @@ 43D9FFD021EAE05D00AF44BF /* LoopCore */ = { isa = PBXGroup; children = ( + 1CFB4FFCC843C243237519A0 /* Extensions */, + 982246E71B12B5C493AD5140 /* Managers */, 3ED3199B2EB65A9B00820BCF /* LiveActivitySettings.swift */, C1ED6C632E7C6DA6002F91C2 /* Models */, C1ED6C602E79BB9C002F91C2 /* NotificationManager.swift */, @@ -2116,7 +2171,6 @@ 89D1503D24B506EB00EDE253 /* Dictionary.swift */, 89CA2B2F226C0161004D9350 /* DirectoryObserver.swift */, A9CBE457248AB564008E7BA2 /* DoseStore+SimulatedCoreData.swift */, - A9B996F127238705002DC09C /* DosingDecisionStore.swift */, A9CBE459248ACBE1008E7BA2 /* DosingDecisionStore+SimulatedCoreData.swift */, 142CB7582A60BF2E0075748A /* EditMode.swift */, A9F703742489C9A000C98AD8 /* GlucoseStore+SimulatedCoreData.swift */, @@ -2124,11 +2178,9 @@ 89E267FE229267DF00A3F2AF /* Optional.swift */, A967D94B24F99B9300CDDF8A /* OutputStream.swift */, A9F703762489D8AA00C98AD8 /* PersistentDeviceLog+SimulatedCoreData.swift */, - A999D40524663D18004C89D4 /* PumpManagerError.swift */, 892A5D682230C41D008961AB /* RangeReplaceableCollection.swift */, A9CBE45B248ACC03008E7BA2 /* SettingsStore+SimulatedCoreData.swift */, C1FB428B217806A300FAB378 /* StateColorPalette.swift */, - C1F2CAA92B76B3EE00D7F581 /* TempBasalRecommendation.swift */, B4C6D2432EAA2AB4006F5755 /* TimeInterval.swift */, 43F89CA222BDFBBC006BB54E /* UIActivityIndicatorView.swift */, 43F41C361D3BF32400C11ED6 /* UIAlertController.swift */, @@ -2486,6 +2538,8 @@ 83081FFB0497A4596F597830 /* Loop/Resources/Sounds/synth.caf */, 21F13EB666EBFDD03F99CB7E /* Loop/Resources/Sounds/trill.caf */, C9EA3F48338B49BD9C6BD0F9 /* Loop/Resources/Sounds/urgent_low.caf */, + A973A0F6CF644FFAA48989FC /* UserDefaults+LoopIntents.swift */, + 42BE0808089545E7BE6AE359 /* NSBundle.swift */, A9B1F564432BCBF70C0C6CBB /* Loop/Resources/Sounds/acoustic_hg_harsh.caf */, 3481DFC7974E27ABE6AAC4C3 /* Loop/Resources/Sounds/acoustic_hg_medium.caf */, 1DCC85EA42C8D22C1EC824D8 /* Loop/Resources/Sounds/acoustic_hg_mild.caf */, @@ -2782,7 +2836,6 @@ 43D9002C21EB225D00AF44BF /* HealthKit.framework */, 43F5C2C81B929C09003EB13D /* HealthKit.framework */, 4344628320A7A3BE00C4BE6F /* LoopKit.framework */, - F8760AF31E3C43F6AFBA1BE7 /* G7SensorKit.framework */, 437AFEE6203688CF008C4892 /* LoopKitUI.framework */, 892A5D5A222F0D7C008961AB /* LoopTestingKit.framework */, C1E2774722433D7A00354103 /* MKRingProgressView.framework */, @@ -2899,6 +2952,14 @@ C1ED6C632E7C6DA6002F91C2 /* Models */ = { isa = PBXGroup; children = ( + A9FB75F0252BE320004C7D3F /* BolusDosingDecision.swift */, + C16F511A2B89363A00EFD7A1 /* SimpleInsulinDose.swift */, + 36421A4B9676EF68CDDC4ECD /* AlgorithmBridge.swift */, + C16F51182B891DB600EFD7A1 /* StoredDataAlgorithmInput.swift */, + A9B996EF27235191002DC09C /* LoopWarning.swift */, + 436A0DA41D236A2A00104B24 /* LoopError.swift */, + C1C660D0252E4DD5009B5C32 /* LoopConstants.swift */, + 8378B7D1F532A5F47CEBB6AE /* CarbEntryChange.swift */, C1DCEDF32E999D5A001A7BB0 /* LastManualBolus.swift */, C1ED6C812E7D8E36002F91C2 /* AcknowledgeAlertUserInfo.swift */, B5C0FFEE2F8A4D2E00A1B2C1 /* WatchUsageSummary.swift */, @@ -2934,11 +2995,7 @@ E95D37FF24EADE68005E2F50 /* Store Protocols */ = { isa = PBXGroup; children = ( - E95D380024EADE7C005E2F50 /* DoseStoreProtocol.swift */, - E95D380224EADF36005E2F50 /* CarbStoreProtocol.swift */, - E95D380424EADF78005E2F50 /* GlucoseStoreProtocol.swift */, E98A55EC24EDD6380008715D /* LatestStoredSettingsProvider.swift */, - E98A55EE24EDD6E60008715D /* DosingDecisionStoreProtocol.swift */, ); path = "Store Protocols"; sourceTree = ""; @@ -3328,6 +3385,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 992345AB303643A60064B448 /* BuildDetails.plist in Resources */, + 992345AC303643A60064B448 /* BuildDetails.swift in Resources */, B66D1F2F2E6A5D6600471149 /* InfoPlist.xcstrings in Resources */, 147EFE8E2A8BCC5500272438 /* DefaultAssets.xcassets in Resources */, B66D1F272E6A5D6500471149 /* Localizable.xcstrings in Resources */, @@ -3418,6 +3477,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + DDBD0002000000000000BD02 /* BuildDetails.plist in Resources */, C1275E1F2E822CEE0013B99D /* DerivedAssets.xcassets in Resources */, B66D1F232E6A5D6500471149 /* InfoPlist.xcstrings in Resources */, 1F0000022FBA000100000001 /* AppIcon.icon in Resources */, @@ -3668,11 +3728,9 @@ B4FEEF7D24B8A71F00A8DF9B /* DeviceDataManager+DeviceStatus.swift in Sources */, 142CB7592A60BF2E0075748A /* EditMode.swift in Sources */, 14C9706E2C5A83AF00E8A01B /* IOBChartView.swift in Sources */, - E95D380324EADF36005E2F50 /* CarbStoreProtocol.swift in Sources */, B470F5842AB22B5100049695 /* StatefulPluggable.swift in Sources */, E98A55ED24EDD6380008715D /* LatestStoredSettingsProvider.swift in Sources */, C1FB428F217921D600FAB378 /* PumpManagerUI.swift in Sources */, - A9B996F227238705002DC09C /* DosingDecisionStore.swift in Sources */, 43C513191E864C4E001547C7 /* GlucoseRangeSchedule.swift in Sources */, B4D904412AA8989100CBD826 /* StatefulPluginManager.swift in Sources */, E9B355292935919E0076AB04 /* MissedMealSettings.swift in Sources */, @@ -3691,7 +3749,6 @@ 14C970822C5C2EC100E8A01B /* FavoriteFoodInsightsView.swift in Sources */, 1D080CBD2473214A00356610 /* AlertStore.xcdatamodeld in Sources */, C11BD0552523CFED00236B08 /* SimpleBolusViewModel.swift in Sources */, - C1F2CAAA2B76B3EE00D7F581 /* TempBasalRecommendation.swift in Sources */, 14C970802C5C0A1500E8A01B /* FavoriteFoodInsightsViewModel.swift in Sources */, 149A28BB2A853E5100052EDF /* CarbEntryViewModel.swift in Sources */, C19008FE25225D3900721625 /* SimpleBolusCalculator.swift in Sources */, @@ -3714,14 +3771,12 @@ 142CB75B2A60BFC30075748A /* FavoriteFoodsView.swift in Sources */, A9D5C5B625DC6C6A00534873 /* LoopAppManager.swift in Sources */, 4302F4E11D4E9C8900F0FCAF /* TextFieldTableViewController.swift in Sources */, - C16F51192B891DB600EFD7A1 /* StoredDataAlgorithmInput.swift in Sources */, 1452F4AB2A851EDF00F8B9E4 /* FavoriteFoodAddEditView.swift in Sources */, C1742332259BEADC00399C9D /* ManualEntryDoseView.swift in Sources */, 43F64DD91D9C92C900D24DC6 /* TitleSubtitleTableViewCell.swift in Sources */, 84213C8D2D949E8100642E78 /* InsulinDeliveryLogEventRow.swift in Sources */, 43FCEEA9221A615B0013DD30 /* StatusChartsManager.swift in Sources */, A9F703752489C9A000C98AD8 /* GlucoseStore+SimulatedCoreData.swift in Sources */, - E95D380524EADF78005E2F50 /* GlucoseStoreProtocol.swift in Sources */, 43E3449F1B9D68E900C85C07 /* StatusTableViewController.swift in Sources */, 84213C892D94941000642E78 /* InsulinDeliveryLogEvent.swift in Sources */, B42D124328D371C400E43D22 /* AlertMuter.swift in Sources */, @@ -3731,7 +3786,6 @@ 43DBF0531C93EC8200B3C386 /* DeviceDataManager.swift in Sources */, A9347F2F24E7508A00C99C34 /* WatchHistoricalCarbs.swift in Sources */, C10509792D8B636900118A37 /* Environment+TemporaryPresetsManager.swift in Sources */, - A9B996F027235191002DC09C /* LoopWarning.swift in Sources */, C17824A01E19CF9800D9D25C /* GlucoseThresholdTableViewController.swift in Sources */, 4372E487213C86240068E043 /* SampleValue.swift in Sources */, 14C970682C5991CD00E8A01B /* LoopChartView.swift in Sources */, @@ -3763,7 +3817,6 @@ 439BED2A1E76093C00B0AED5 /* CGMManager.swift in Sources */, C16B983E26B4893300256B05 /* DoseEnactor.swift in Sources */, 3ED319962EB65A5C00820BCF /* LiveActivityManagementViewModel.swift in Sources */, - E98A55EF24EDD6E60008715D /* DosingDecisionStoreProtocol.swift in Sources */, B4001CEE28CBBC82002FB414 /* AlertManagementView.swift in Sources */, A1E47000000000000000A001 /* AlertHistoryView.swift in Sources */, C9A47B0380AAB1FC572D3F00 /* GlucoseAlertSettingsView.swift in Sources */, @@ -3805,7 +3858,6 @@ 84DF48BD2F6A0AE200BEDB40 /* TranscriptView.swift in Sources */, B43B5C562EAFBF230096A6AE /* RecentGlucoseTableViewCell.swift in Sources */, C10509752D8B309400118A37 /* Environment+SettingsManager.swift in Sources */, - E95D380124EADE7C005E2F50 /* DoseStoreProtocol.swift in Sources */, 43D381621EBD9759007F8C8F /* HeaderValuesTableViewCell.swift in Sources */, C13DA2B024F6C7690098BB29 /* UIViewController.swift in Sources */, A9C62D892331703100535612 /* LoggingServicesManager.swift in Sources */, @@ -3835,7 +3887,6 @@ E4E2B3856D5DA256AA09C31E /* ShareOutcomesViewModel.swift in Sources */, 69C4F11D6759B7FED4F68BC4 /* ShareOutcomesView.swift in Sources */, D7DECBB118052B85680A241B /* SetupComparison.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 */, @@ -3859,7 +3910,6 @@ BAF043BF0BC70EC99AE79F71 /* HealthAccessView.swift in Sources */, A9B607B0247F000F00792BE4 /* UserNotifications+Loop.swift in Sources */, 43F89CA322BDFBBD006BB54E /* UIActivityIndicatorView.swift in Sources */, - A999D40624663D18004C89D4 /* PumpManagerError.swift in Sources */, 437D9BA31D7BC977007245E8 /* PredictionTableViewController.swift in Sources */, A987CD4924A58A0100439ADC /* ZipArchive.swift in Sources */, 43F41C371D3BF32400C11ED6 /* UIAlertController.swift in Sources */, @@ -3874,7 +3924,6 @@ C1DE5D23251BFC4D00439E49 /* SimpleBolusView.swift in Sources */, 84213C8B2D94948900642E78 /* InsulinDeliveryOverview.swift in Sources */, 89ADE13B226BFA0F0067222B /* TestingScenariosManager.swift in Sources */, - 436A0DA51D236A2A00104B24 /* LoopError.swift in Sources */, 4F11D3C220DD80B3006E072C /* WatchHistoricalGlucose.swift in Sources */, 14ED83F62C6421F9008B4A5C /* FavoriteFoodInsightsCardView.swift in Sources */, C174233C259BEB0F00399C9D /* ManualEntryDoseViewModel.swift in Sources */, @@ -3889,8 +3938,6 @@ 4F70C2101DE8FAC5006380B7 /* ExtensionDataManager.swift in Sources */, 1455ACB22C667BEE004F44F2 /* Deeplink.swift in Sources */, 43DFB62320D4CAE7008A7BAE /* PumpManager.swift in Sources */, - A9FB75F1252BE320004C7D3F /* BolusDosingDecision.swift in Sources */, - 2437A340C1560EEDE55E3F7B /* CarbEntryChange.swift in Sources */, 892A5D59222F0A27008961AB /* Debug.swift in Sources */, 431A8C401EC6E8AB00823B9C /* CircleMaskView.swift in Sources */, 1D05219D2469F1F5000EBBDE /* AlertStore.swift in Sources */, @@ -3914,7 +3961,6 @@ 8968B1122408B3520074BB48 /* UIFont.swift in Sources */, 1452F4A92A851C9400F8B9E4 /* FavoriteFoodAddEditViewModel.swift in Sources */, 438D42FB1D7D11A4003244B0 /* PredictionInputEffectTableViewCell.swift in Sources */, - C1C660D1252E4DD5009B5C32 /* LoopConstants.swift in Sources */, 432E73CB1D24B3D6009AD15D /* RemoteDataServicesManager.swift in Sources */, 7E69CFFC2A16A77E00203CBD /* ResetLoopManager.swift in Sources */, B40D07C7251A89D500C1C6D7 /* GlucoseDisplay.swift in Sources */, @@ -3922,7 +3968,6 @@ 14C970862C5C358C00E8A01B /* FavoriteFoodInsightsChartsView.swift in Sources */, 5021CA8C264146C9DFF33E9D /* PluginPickerView.swift in Sources */, 17EA66859884C0208DED47CF /* LoopKitTypeAliases.swift in Sources */, - 613BF2B78104AB9694590C65 /* AlgorithmBridge.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4031,6 +4076,24 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 527427E1C81FAB157FEEA18B /* BolusDosingDecision.swift in Sources */, + 3EC6DF601CC5B95DD71BB52D /* TempBasalRecommendation.swift in Sources */, + DEAABF073E29356AF4D5CAEB /* SimpleInsulinDose.swift in Sources */, + 46CCF5F0CA2280F458F65B80 /* StoredDataAlgorithmInput.swift in Sources */, + 187E5B111D2877B585483489 /* SettingsProvider.swift in Sources */, + 6AE03D0437B61A334F2FE2E4 /* PumpManagerError.swift in Sources */, + CF8C0C366A2EB7DBF96BA005 /* DosingDecisionStore.swift in Sources */, + EAF0BAC73C927B8C75B5CEE7 /* LoopWarning.swift in Sources */, + 4AA88667600C539BA7556DC6 /* LoopError.swift in Sources */, + A11291908633A17B3F06CE29 /* LoopConstants.swift in Sources */, + 8C6801E4F5D9D5A8F614FB1F /* CarbEntryChange.swift in Sources */, + 1FFB250EDF89EBC360A7A5C1 /* GlucoseStoreProtocol.swift in Sources */, + E0F0D982F2ACD9D78617BA0E /* DosingDecisionStoreProtocol.swift in Sources */, + 451FFEF51B975CE332CC6C6D /* DoseStoreProtocol.swift in Sources */, + 0390983B63120E47B543264F /* CarbStoreProtocol.swift in Sources */, + D96DC2868F8E7ADEEA335047 /* LoopAlgorithmSupport.swift in Sources */, + 01F99A06F17502CC60E9535C /* AlgorithmBridge.swift in Sources */, + 229FEACAEAB2DF60B3DB5C84 /* LoopCycle.swift in Sources */, C1ED6C822E7D8E3D002F91C2 /* AcknowledgeAlertUserInfo.swift in Sources */, B5C0FFEE2F8A4D2E00A1B2C2 /* WatchUsageSummary.swift in Sources */, E9C00EF324C6222400628F35 /* LoopSettings.swift in Sources */, @@ -4068,6 +4131,24 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + B2F374A7AC2458B5AC5CE371 /* BolusDosingDecision.swift in Sources */, + CEEBC321F91A87AFC45E1015 /* TempBasalRecommendation.swift in Sources */, + 0921C6BF71476F50FAC0C7F2 /* SimpleInsulinDose.swift in Sources */, + 81A328004D241282EA326E69 /* StoredDataAlgorithmInput.swift in Sources */, + A3BAB5421752C7A8E1E70D7F /* SettingsProvider.swift in Sources */, + 36457634F78A15F4CCFB2A7C /* PumpManagerError.swift in Sources */, + 77E1CC783E13DB6571487416 /* DosingDecisionStore.swift in Sources */, + 1EF7ABFF44F28418B9081884 /* LoopWarning.swift in Sources */, + D5321AAC567B470277F4693C /* LoopError.swift in Sources */, + CA7D019306B2674E4BAF6DE3 /* LoopConstants.swift in Sources */, + 59A9DDD71F0CB765C3529F9C /* CarbEntryChange.swift in Sources */, + 08B8082DACB7E55849F34C32 /* GlucoseStoreProtocol.swift in Sources */, + 931E35F788B8CF54BC5277C8 /* DosingDecisionStoreProtocol.swift in Sources */, + 690482C3E6D6A94D7BA232B6 /* DoseStoreProtocol.swift in Sources */, + 3CD16E39CAB99674C3AB351C /* CarbStoreProtocol.swift in Sources */, + 104021B7449DA59717EA4E5D /* LoopAlgorithmSupport.swift in Sources */, + 52CB9A826E3B3DFC4E483084 /* AlgorithmBridge.swift in Sources */, + D23A31DF90992466C4626B34 /* LoopCycle.swift in Sources */, C1ED6C832E7D8E3D002F91C2 /* AcknowledgeAlertUserInfo.swift in Sources */, B5C0FFEE2F8A4D2E00A1B2C3 /* WatchUsageSummary.swift in Sources */, C1ED6C652E7C6E2F002F91C2 /* CarbBackfillRequestUserInfo.swift in Sources */, diff --git a/Loop/Extensions/BasalRelativeDose.swift b/Loop/Extensions/BasalRelativeDose.swift index 41497fdd38..e426f7d5e0 100644 --- a/Loop/Extensions/BasalRelativeDose.swift +++ b/Loop/Extensions/BasalRelativeDose.swift @@ -9,6 +9,7 @@ import Foundation import LoopKit import LoopAlgorithm +import LoopKitUI public extension Array where Element == BasalRelativeDose { func trimmed(from start: Date? = nil, to end: Date? = nil) -> [BasalRelativeDose] { @@ -50,3 +51,14 @@ extension BasalRelativeDose { ) } } + +extension LoopAlgorithm.BasalRelativeDose { + var chartedDose: ChartedDose { + let type: ChartedDose.DoseType + switch self.type { + case .bolus: type = .bolus + case .basal: type = .basal + } + return ChartedDose(type: type, startDate: startDate, endDate: endDate, volume: volume, netBasalUnits: netBasalUnits) + } +} diff --git a/Loop/Localizable.xcstrings b/Loop/Localizable.xcstrings index e8be2e8501..92601ae0e8 100644 --- a/Loop/Localizable.xcstrings +++ b/Loop/Localizable.xcstrings @@ -9061,131 +9061,6 @@ }, "Basal Rate" : { - }, - "Basal Rate Schedule" : { - "comment" : "Details for configuration error when basal rate schedule is missing", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "جدول الضخ المستمر" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Basalrateskema" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Basalraten-Plan" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Perfil Basal" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Basaaliohjelma" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Programme débit basal" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "Basal Rate Schedule" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tabella di velocità basale" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "基礎レートスケジュール" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tidsplan for basaldoser" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Basaalstandenschema" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Harmonogram dawki standardowej" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Programação da Taxa Basal" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Orar rate bazale" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Скорости базала" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Harmonogram bazálnej dávky" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Basaldosschema" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bazal Oran Çizelgesi" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lịch biểu tiêm liều basal" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "基础率表" - } - } - } }, "Basal Rates" : { "comment" : "The title of the basal rate profile screen\n The title text for the basal rate schedule", @@ -10887,131 +10762,6 @@ } } }, - "Carb effects" : { - "comment" : "Details for missing data error when carb effects are missing", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "تأثيرات الكارب" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kulhydrateffekter" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kohlenhydrat-Wirkungen" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Efectos de carbohidratos" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hiilihydraattivaikutus" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Effets des glucides" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "השפעות פחמימות" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Effetto carboidrati" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "糖質効果" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Karb effekter" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Koolhydraateffecten" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wpływ węglowodanów" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Efeitos Carb" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Efecte carbohidrați" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Влияние углеводов" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Účinky sacharidov" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kolhydrateffekter" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Karbonhidrat etkileri" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "các tác động của Carb" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "碳水效果" - } - } - } - }, "Carb Entry" : { "comment" : "Label for carb entry row on bolus screen", "localizations" : { @@ -11163,175 +10913,98 @@ "Carb Ratio" : { }, - "Carb Ratio Schedule" : { - "comment" : "Details for configuration error when carb ratio schedule is missing", + "Carb Ratios" : { + "comment" : "The title of the carb ratios schedule screen\n The title text for the carb ratio schedule", + "extractionState" : "manual", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "معاملات الكارب" + } + }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Tidsplan for kulhydratforhold" + "value" : "Kulhydratforhold" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Zeitplan für das Kohlenhydratverhältnis" + "value" : "KH-Verhältnis" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Carb Ratios" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Calendario de ratio de carbohidratos" + "value" : "Ratios de carbohidratos" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hiilihydraattisuhteet" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Programme des ratios Insuline-Glucides" + "value" : "Ratios Insuline-Glucides" } }, - "it" : { + "he" : { "stringUnit" : { "state" : "translated", - "value" : "Tabella del rapporto carboidrati" + "value" : "Carb Ratios" } }, - "nb" : { + "it" : { "stringUnit" : { "state" : "translated", - "value" : "Tidsplan for karbohydratforhold" + "value" : "Rapporti carboidrati" } }, - "nl" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Koolhydraatratioschema" + "value" : "Carb Ratios" } }, - "pl" : { + "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Harmonogram Współczynnika Węglowodanowego" + "value" : "Karb forhold" } }, - "ro" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Programul raportului de carbohidrați" + "value" : "Koolhydraatratio's" } }, - "ru" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Расписание соотношения углеводов" + "value" : "Współczynniki węglowodanowe" } }, - "tr" : { + "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Karb Oranı Programı" + "value" : "Taxas de Carbs" } }, - "zh-Hans" : { + "ro" : { "stringUnit" : { "state" : "translated", - "value" : "碳水系数配置表" - } - } - } - }, - "Carb Ratios" : { - "comment" : "The title of the carb ratios schedule screen\n The title text for the carb ratio schedule", - "extractionState" : "manual", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "معاملات الكارب" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kulhydratforhold" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "KH-Verhältnis" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Carb Ratios" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ratios de carbohidratos" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hiilihydraattisuhteet" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ratios Insuline-Glucides" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "Carb Ratios" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rapporti carboidrati" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "Carb Ratios" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Karb forhold" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Koolhydraatratio's" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Współczynniki węglowodanowe" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Taxas de Carbs" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Raport carbohidrați insulină" + "value" : "Raport carbohidrați insulină" } }, "ru" : { @@ -12429,125 +12102,6 @@ } } }, - "Check your CGM data source" : { - "comment" : "Recovery suggestion when glucose data is missing", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "تأكد من مصدر قراءات السكر المستمرة" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kontroller din CGM-datakilde" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Überprüfe Deine CGM-Datenquelle." - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verifique su fuente de datos CGM" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tarkista CGM-tietolähde" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vérifier votre source de données CGM" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "בדוק את מקור המידע שלך בחיישן" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Controlla la sorgente dati del sensore" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "CGM データソースを確認してください" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sjekk CGM" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Controleer je CGM gegevensbron" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sprawdź swój CGM" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verifique seu dispositivo CGM" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verificați sursa de date a senzorului de glicemie" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Проверьте источник мониторинга СК" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kontrollera din CGM:s datakälla" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "CGM veri kaynağınızı kontrol edin" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kiểm tra nguồn dữ liệu CGM của bạn" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "请检查你的动态血糖数据来源" - } - } - } - }, "Check your device time and/or remove any invalid data from Apple Health." : { "comment" : "Caption for bolus screen notice when glucose data is in the future", "localizations" : { @@ -13871,204 +13425,85 @@ } } }, - "Configuration Error: %1$@" : { - "comment" : "The error message displayed for configuration errors. (1: configuration error details)", + "Configure Display" : { + "comment" : "Title for the view to configure the lock screen display", "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "خطأ في المعطيات: %1$@" - } - }, - "da" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "Konfigurationsfejl: %1$@" + "value" : "Anzeige konfigurieren" } }, - "de" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Konfigurationsfehler: %1$@" + "value" : "Configurer l'affichage" } }, - "es" : { + "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Error de Configuración: %1$@" + "value" : "Tilpass skjermen" } }, - "fi" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Määritysvirhe: %1$@" + "value" : "Scherm configureren" } }, - "fr" : { + "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Erreur de Configuration: %1$@" + "value" : "Configurați afișajul" } }, - "he" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Configuration Error: %1$@" + "value" : "显示设置" } - }, - "it" : { + } + } + }, + "Configure Lock Screen / Carplay Row" : { + "comment" : "A link that takes the user to a view where they can configure the display of the live activity screen on their lock screen and in CarPlay.", + "isCommentAutoGenerated" : true, + "localizations" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "Errore di configurazione: %1$@" + "value" : "Sperrbildschirm / CarPlay-Zeile konfigurieren" } }, - "ja" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "設定エラー: %1$@" + "value" : "Configurer l'écran de verrouillage / CarPlay" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Konfigurasjonsfeil: %1$@" + "value" : "Tilpass låseskjerm / Carplay rad" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Configuratiefout: %1$@" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Błąd konfiguracji: %1$@" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Erro de Configuração: %1$@" + "value" : "Rij voor toegangsscherm / CarPlay instellen" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Eroare configurare: %1$@" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ошибка конфигурации: %1$@" + "value" : "Configurați ecranul de blocare / rândul CarPlay" } }, - "sv" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Konfigurationsfel %1$@" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Konfigürasyon Hatası: %1$@" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lỗi cấu hình: %1$@" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "配置错误:%1$@" - } - } - } - }, - "Configure Display" : { - "comment" : "Title for the view to configure the lock screen display", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Anzeige konfigurieren" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configurer l'affichage" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tilpass skjermen" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Scherm configureren" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configurați afișajul" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "显示设置" - } - } - } - }, - "Configure Lock Screen / Carplay Row" : { - "comment" : "A link that takes the user to a view where they can configure the display of the live activity screen on their lock screen and in CarPlay.", - "isCommentAutoGenerated" : true, - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sperrbildschirm / CarPlay-Zeile konfigurieren" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configurer l'écran de verrouillage / CarPlay" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tilpass låseskjerm / Carplay rad" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rij voor toegangsscherm / CarPlay instellen" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configurați ecranul de blocare / rândul CarPlay" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "配置锁屏 / CarPlay 显示行" + "value" : "配置锁屏 / CarPlay 显示行" } } } @@ -21376,510 +20811,272 @@ "Glucose Change Chart" : { }, - "Glucose data is %1$@ old" : { - "comment" : "The error message when glucose data is too old to be used. (1: glucose data age in minutes)", + "Glucose Data Now Available" : { + "comment" : "Alert title when glucose data returns while on bolus screen", + "extractionState" : "stale", "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "قراءات السكر منذ %1$@ " - } - }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Blodsukkerdata er %1$@ gamle" + "value" : "Blodsukkerdata er nu tilgængelige" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Blutzuckerdaten sind %1$@ alt" + "value" : "Blutzucker jetzt verfügbar" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Los datos de glucosa son de hace %1$@ " + "value" : "Datos de glucosa ahora disponibles" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Glukoositieto on %1$@ vanha" + "value" : "Glukoositiedot nyt saatavilla" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Les dernières données de glucose remontent à %1$@" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "Glucose data is %1$@ old" + "value" : "Les données de glycémie sont maintenant disponibles" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "I dati sulla glicemia sono vecchi di %1$@" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "血糖データは %1$@ 前のものです" + "value" : "Dati glicemia ora disponibili" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Blodsukkerdata er %1$@ gammel" + "value" : "Blodsukkerdata er utilgjengelig" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Glucosegegevens zijn %1$@ oud" + "value" : "Glucosegegevens nu beschikbaar" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Dane o glukozie są nieaktualne od %1$@" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Medida de glicose está %1$@ atrasada" + "value" : "Dane dotyczące glukozy są już dostępne" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Datele despre glicemie au o vechime de %1$@" + "value" : "Valorile glicemiei sunt disponibile" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Данные о глюкозе устарели на %1$@" + "value" : "Данные о глюкозе теперь доступны" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Glukosvärde är %1$@ gammalt" + "value" : "Blodglukosvärde finns nu tillgängligt" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ eski KŞ verisi" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dữ liệu đường huyết %1$@ cũ" + "value" : "KŞ Verileri Artık Kullanılabilir" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "葡萄糖%1$@分钟未更新" + "value" : "现已获取血糖数据" } } } }, - "Glucose data not available" : { - "comment" : "Description of error when glucose data is missing", + "Glucose effect of suspending insulin delivery" : { + "comment" : "Description of the prediction input effect for suspension of insulin delivery", "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "قراءات السكر غير متوفرة" - } - }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Blodsukkerdata ikke tilgængelige" + "value" : "Glukoseeffekt ved afbrydelse af insulintilførsel" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Blutzuckerdaten nicht verfügbar" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Los datos de glucosa no están disponibles" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Glukoositietoja ei saatavilla" + "value" : "Glukoseeffekt der Unterbrechung der Insulinabgabe" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Les données de glucose ne sont pas disponibles" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "Glucose data not available" + "value" : "Effet sur la glycémie de la suspension d'administration de l'insuline" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Dati sulla glicemia non disponibili" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "グルコースデータがありません" + "value" : "Effetto sulla glicemia della sospensione dell'erogazione d'insulina" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Blodsukkerdata er utilgjengelig" + "value" : "Glukoseeffekt av å suspendere insulintilførsel" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Glucosegegevens niet beschikbaar" + "value" : "Glucose-effect van onderbroken insulinetoediening" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Dane o glukozie są niedostępne" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Medida de glicose não disponível" + "value" : "Wpływ wstrzymania podawania insuliny na poziom glukozy" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Nu sunt disponibile date despre glicemie" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Данные гликемии недоступны" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Glukosvärde saknas" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "KŞ verisi mevcut değil" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dữ liệu đường huyết không sẵn sàng" + "value" : "Efectul asupra glicemiei al suspendării administrării de insulină" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "血糖数据不可用" + "value" : "暂停胰岛素输注对血糖的影响" } } } }, - "Glucose Data Now Available" : { - "comment" : "Alert title when glucose data returns while on bolus screen", - "extractionState" : "stale", + "Glucose Entry Out of Range" : { + "comment" : "Alert title for a manual glucose entry out of range error\nTitle for bolus screen warning when glucose entry is out of range", "localizations" : { "da" : { "stringUnit" : { "state" : "translated", - "value" : "Blodsukkerdata er nu tilgængelige" + "value" : "Blodsukkerværdi er uden for intervallet" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Blutzucker jetzt verfügbar" + "value" : "Blutzucker-Eingabe außerhalb des Bereichs" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Datos de glucosa ahora disponibles" + "value" : "Entrada de glucosa fuera de rango" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Glukoositiedot nyt saatavilla" + "value" : "Glukoosiarvo alueen ulkopuolella" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Les données de glycémie sont maintenant disponibles" + "value" : "La glycémie saisie est hors de la plage." } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Dati glicemia ora disponibili" + "value" : "Glicemia inserita fuori intervallo" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Blodsukkerdata er utilgjengelig" + "value" : "Blodsukkerdata er utenfor intervallet" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Glucosegegevens nu beschikbaar" + "value" : "Glucose-invoer buiten bereik" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Dane dotyczące glukozy są już dostępne" + "value" : "Wprowadzanie glukoza jest poza zakresem" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Valorile glicemiei sunt disponibile" + "value" : "Valoarea glicemiei introdusă este în afara intervalului" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Данные о глюкозе теперь доступны" + "value" : "Ввод глюкозы вне допустимого диапазона" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Blodglukosvärde finns nu tillgängligt" + "value" : "Det manuellt inmatade blodsockervärdet utanför gränsvärden" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "KŞ Verileri Artık Kullanılabilir" + "value" : "KŞ Girişi Aralık Dışında" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "现已获取血糖数据" + "value" : "输入血糖超出范围" } } } }, - "Glucose effect of suspending insulin delivery" : { - "comment" : "Description of the prediction input effect for suspension of insulin delivery", + "Glucose Metrics" : { + "comment" : "Glucose metrics section header" + }, + "Glucose Momentum" : { + "comment" : "Title of the prediction input effect for glucose momentum", "localizations" : { - "da" : { + "ar" : { "stringUnit" : { "state" : "translated", - "value" : "Glukoseeffekt ved afbrydelse af insulintilførsel" + "value" : "مقاومة سكر الدم" } }, - "de" : { + "da" : { "stringUnit" : { "state" : "translated", - "value" : "Glukoseeffekt der Unterbrechung der Insulinabgabe" + "value" : "Blodsukkermomentum" } }, - "fr" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "Effet sur la glycémie de la suspension d'administration de l'insuline" + "value" : "Blutzucker-Momentum" } }, - "it" : { + "es" : { "stringUnit" : { "state" : "translated", - "value" : "Effetto sulla glicemia della sospensione dell'erogazione d'insulina" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Glukoseeffekt av å suspendere insulintilførsel" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Glucose-effect van onderbroken insulinetoediening" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wpływ wstrzymania podawania insuliny na poziom glukozy" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Efectul asupra glicemiei al suspendării administrării de insulină" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "暂停胰岛素输注对血糖的影响" - } - } - } - }, - "Glucose Entry Out of Range" : { - "comment" : "Alert title for a manual glucose entry out of range error\nTitle for bolus screen warning when glucose entry is out of range", - "localizations" : { - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Blodsukkerværdi er uden for intervallet" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Blutzucker-Eingabe außerhalb des Bereichs" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Entrada de glucosa fuera de rango" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Glukoosiarvo alueen ulkopuolella" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "La glycémie saisie est hors de la plage." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Glicemia inserita fuori intervallo" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Blodsukkerdata er utenfor intervallet" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Glucose-invoer buiten bereik" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wprowadzanie glukoza jest poza zakresem" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Valoarea glicemiei introdusă este în afara intervalului" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ввод глюкозы вне допустимого диапазона" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Det manuellt inmatade blodsockervärdet utanför gränsvärden" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "KŞ Girişi Aralık Dışında" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "输入血糖超出范围" - } - } - } - }, - "Glucose Metrics" : { - "comment" : "Glucose metrics section header" - }, - "Glucose Momentum" : { - "comment" : "Title of the prediction input effect for glucose momentum", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "مقاومة سكر الدم" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Blodsukkermomentum" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Blutzucker-Momentum" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Momento de Glucosa" + "value" : "Momento de Glucosa" } }, "fi" : { @@ -21979,83 +21176,6 @@ }, "Glucose Summary" : { - }, - "Glucose Target Range Schedule" : { - "comment" : "Details for configuration error when glucose target range schedule is missing", - "localizations" : { - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tidsplan for glukosemålområde" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zeitplan für den Blutzucker-Zielbereich" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Horario para el objetivo de glucosa" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Horaire de la plage cible de glycémie" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Programma di intervalli target di glicemia" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tidsplan for blodsukker målområde" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Glucosestreefbereikschema" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Harmonogram zakresu docelowego glukozy" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Programul intervalului țintă de glicemie" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "График целевого диапазона глюкозы" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "KŞ Hedef Aralığı Programı" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "血糖目标范围表" - } - } - } }, "Glucose, Insulin, and Carb Data Access" : { "comment" : "Apple Health settings descriptive text" @@ -23451,256 +22571,137 @@ "Insulin Delivery Log" : { }, - "Insulin effects" : { - "comment" : "Details for missing data error when insulin effects are missing\nDetails for missing data error when insulin effects including pending insulin are missing", + "Insulin Event" : { + + }, + "Insulin Model" : { + "comment" : "Details for configuration error when insulin model is missing\n The title text for the insulin model setting row", + "extractionState" : "manual", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "تأثيرات الأنسولين" + "value" : "نوع الأنسولين" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Insulineffekter" + "value" : "Insulinmodel" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Insulinwirkungen" + "value" : "Insulin-Modell" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulin Model" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Efectos de la insulina" + "value" : "Modelo de Insulina" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Insuliinivaikutus" + "value" : "Insuliinimalli" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Effets de l'insuline" + "value" : "Modèle d'insuline" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "Insulin effects" + "value" : "Insulin Model" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Effetto dell’insulina" + "value" : "Modello di azione dell'Insulina" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "インスリン効果" + "value" : "インスリンモデル" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Insulin effekt" + "value" : "Insulin Modell" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Insulineëffecten" + "value" : "Insulinemodel" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "wpływ insuliny" + "value" : "Model insuliny" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Efeitos da Insulina" + "value" : "Modelo de Insulina" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Efecte insulină" + "value" : "Model insulină" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Влияние инсулина" + "value" : "Модель инсулина" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inzulínový model" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Insulineffekter" + "value" : "Insulinmodell" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "İnsülin etkileri" + "value" : "İnsülin Modeli" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Tác dụng của insulin" + "value" : "Chủng loại Insulin" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "胰岛素效果" - } - } - } - }, - "Insulin Event" : { - - }, - "Insulin Model" : { - "comment" : "Details for configuration error when insulin model is missing\n The title text for the insulin model setting row", - "extractionState" : "manual", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "نوع الأنسولين" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulinmodel" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulin-Modell" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulin Model" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Modelo de Insulina" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insuliinimalli" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Modèle d'insuline" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulin Model" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Modello di azione dell'Insulina" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "インスリンモデル" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulin Modell" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulinemodel" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Model insuliny" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Modelo de Insulina" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Model insulină" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Модель инсулина" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Inzulínový model" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulinmodell" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "İnsülin Modeli" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Chủng loại Insulin" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "胰岛素模型" + "value" : "胰岛素模型" } } } @@ -23931,83 +22932,6 @@ }, "Insulin Sensitivity Factor (ISF)" : { - }, - "Insulin Sensitivity Schedule" : { - "comment" : "Details for configuration error when insulin sensitivity schedule is missing", - "localizations" : { - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulinfølsomhed tidsplan" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulinempfindlichkeit Plan" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Horario de la sensibilidad a la insulina" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Horaire de facteur de sensibilité à l'insuline" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Programma di sensibilità insulinica" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulin følsomhet tidsplan" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulinegevoeligheidschema" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Harmonogram wrażliwości na insulinę" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Orarul Factorului de Sensibilitate la Insulină" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "График чувствительности к инсулину" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "İnsülin Duyarlılık Programı" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "胰岛素敏感系数(ISF)方案" - } - } - } }, "Insulin Suspended" : { "comment" : "The title of the cell indicating the pump is suspended", @@ -24837,83 +23761,6 @@ } } }, - "Invalid glucose reading with a timestamp that is %1$@ in the future" : { - "comment" : "The error message when glucose data is in the future. (1: glucose data time in future in minutes)", - "localizations" : { - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ugyldig glukoseaflæsning med et tidsstempel, der er %1$@ i fremtiden" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ungültiger Blutzuckermesswert mit einem Zeitstempel, der %1$@ in der Zukunft liegt" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lectura de glucosa no válida de %1$@ en el futuro" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lecture de glucose non valide avec un horodatage situé à %1$@ dans le futur" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lettura della glicemia non valida con data e ora %1$@ nel futuro" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ugyldig blodsukkermåling med et tidsstempel som er %1$@ i fremtiden" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ongeldige glucosemeting met een tijdstempel dat %1$@ in de toekomst ligt." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nieprawidłowy odczyt glukozy ze znacznikiem czasu, który wynosi %1$@ w przyszłości" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Citirea invalidă a glicemiei cu un marcaj temporal care este %1$@ în viitor" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Неверное показание глюкозы с временной меткой, которая находится на %1$@ в будущем" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gelecekte %1$@ olan bir zaman damgasıyla geçersiz KŞ okuması" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "血糖读数无效,其时间戳为 %1$@ ,该时间点在未来" - } - } - } - }, "iOS" : { }, @@ -26712,9 +25559,6 @@ } } }, - "Loop is already looping." : { - "comment" : "The error message displayed for LoopError.loopInProgress errors." - }, "Loop normally gives 40% of your predicted insulin needs each dosing cycle.\n\nWhen the Glucose Based Partial Application experiment is enabled, Loop will vary the percentage of recommended bolus delivered each cycle with glucose level.\n\nNear correction range, it will use 20% (similar to Temp Basal), and gradually increase to a maximum of 80% at high glucose (200 mg/dL, 11.1 mmol/L).\n\nPlease be aware that during fast rising glucose, such as after an unannounced meal, this feature, combined with velocity and retrospective correction effects, may result in a larger dose than your ISF would call for." : { "comment" : "Description of Glucose Based Partial Application toggle.", "localizations" : { @@ -27357,205 +26201,6 @@ } } }, - "Manual temporary basal in progress. Automatic dosing is disabled." : { - "comment" : "The error message displayed for LoopError.manualTempBasalRunning errors." - }, - "Maximum Basal Rate Per Hour" : { - "comment" : "Details for configuration error when maximum basal rate per hour is missing", - "localizations" : { - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Maksimal basalrate pr. time" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Maximale Basalte pro Stunde" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tasa basal máxima por hora" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Débit Basal Maximum par heure" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "בזאלי מקסימלי לשעה" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Velocità basale massima all'ora" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Maksimal basaldose pr. time" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Maximale basaalstand per uur" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Maksymalna dawka podstawowa na godzinę" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rata bazală maximă pe oră" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Максимальная базальная скорость в час" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Saat Başı Maksimum Bazal Oran" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "每小时最大基础速率" - } - } - } - }, - "Maximum Bolus" : { - "comment" : "Details for configuration error when maximum bolus is missing", - "localizations" : { - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Maksimal bolus" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Maximaler Bolus" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bolo Máximo" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Suurin sallittu bolus" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bolus Maximum" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "בולוס מקסימלי" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bolo massimo" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "最大ボーラス" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Maks bolus" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Maximale bolus" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Maksymalny bolus" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bolus Máximo" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bolus maxim" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Максимальный Болюс" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Maximal bolus" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Maksimum Bolus" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Liều Bolus tối đa" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "最大剂量" - } - } - } - }, "Maximum Bolus Exceeded" : { "comment" : "Title for bolus screen warning when max bolus is exceeded", "localizations" : { @@ -27958,291 +26603,172 @@ } } }, - "Missing data: %1$@" : { - "comment" : "The error message for missing data. (1: missing data details)", + "Missing maximum allowed bolus in settings" : { + "comment" : "Bolus error description: missing maximum bolus in settings.", "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "بيانات مفقودة: %1$@" - } - }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Manglende data: %1$@" + "value" : "Manglende maksimalt tilladt bolus i indstillingerne" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Fehlende Daten: %1$@" + "value" : "Maximal erlaubter Bolus in den Einstellungen fehlt" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Faltan Datos: %1$@" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tiedot puuttuvat: %1$@" + "value" : "Falta el bolo máximo permitido en la configuración" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Données manquantes: %1$@" + "value" : "Bolus maximal autorisé manquant dans les paramètres" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "נתונים חסרים: %1$@" + "value" : "בולוס מקסימלי חסר בהגדרות" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Dati mancanti: %1$@" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "データがありません: %1$@" + "value" : "Manca il bolo massimo consentito nelle impostazioni" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Manglende data: %1$@" + "value" : "Mangler maksimalt tillatt bolus i innstillingene" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Ontbrekende gegevens: %1$@" + "value" : "Maximaal toegestane bolus ontbreekt in instellingen" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Brakujące dane: %1$@" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dados ausentes: %1$@" + "value" : "Brak ustawionego maksymalnego dozwolonego bolusa" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Date lipsă: %1$@" + "value" : "Lipsește bolusul maxim permis în setări" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Пропущены данные: %1$@" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Saknar data: %1$@" + "value" : "Отсутствие максимально допустимого болюса в настройках" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Eksik veri: %1$@" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dữ liệu thiếu: %1$@" + "value" : "Ayarlarda izin verilen maksimum bolus eksik" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "数据缺失: %1$@" + "value" : "设置中缺少最大剂量限制" } } } }, - "Missing maximum allowed bolus in settings" : { - "comment" : "Bolus error description: missing maximum bolus in settings.", + "Mixed-intensity exercise may cause only small changes in glucose levels. Your glucose may go up or down." : { + + }, + "mmol/L" : { + "comment" : "The short unit display string for millimoles of glucose per liter", + "extractionState" : "stale", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "mmol/L" + } + }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Manglende maksimalt tilladt bolus i indstillingerne" + "value" : "mmol/L" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Maximal erlaubter Bolus in den Einstellungen fehlt" + "value" : "mmol/L" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Falta el bolo máximo permitido en la configuración" + "value" : "mmol/L" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "mmol/L" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Bolus maximal autorisé manquant dans les paramètres" + "value" : "mmol/L" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "בולוס מקסימלי חסר בהגדרות" + "value" : "mmol/L" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Manca il bolo massimo consentito nelle impostazioni" + "value" : "mmol/L" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "mmol/L" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Mangler maksimalt tillatt bolus i innstillingene" + "value" : "mmol/L" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Maximaal toegestane bolus ontbreekt in instellingen" + "value" : "mmol/L" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Brak ustawionego maksymalnego dozwolonego bolusa" + "value" : "mmol/L" } }, - "ro" : { + "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Lipsește bolusul maxim permis în setări" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Отсутствие максимально допустимого болюса в настройках" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ayarlarda izin verilen maksimum bolus eksik" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "设置中缺少最大剂量限制" - } - } - } - }, - "Mixed-intensity exercise may cause only small changes in glucose levels. Your glucose may go up or down." : { - - }, - "mmol/L" : { - "comment" : "The short unit display string for millimoles of glucose per liter", - "extractionState" : "stale", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "mmol/L" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "mmol/L" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "mmol/L" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "mmol/L" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "mmol/L" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "mmol/L" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "mmol/L" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "mmol/L" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "mmol/L" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "mmol/L" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "mmol/L" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "mmol/L" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "mmol/L" + "value" : "mmol/L" } }, "ro" : { @@ -28330,125 +26856,6 @@ } } }, - "Momentum effects" : { - "comment" : "Details for missing data error when momentum effects are missing", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "تأثيرات النشاط" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Momentumeffekter" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Momentum-Effekte" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Efectos de Momento" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Liikevaikutukset (momentum)" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Effets de momentum" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "השפעות מומנטום" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Effetti del momento" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "モメンタム効果" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Momentum effekter" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Trendlijneffecten" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "wpływ pędu" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Efeitos de aceleração" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Efecte avânt" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Влияние динамики СК" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Momentumeffekter" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Momentum etkileri" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hiệu ứng động lượng" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "葡萄糖动量效应" - } - } - } - }, "Monitor your glucose and active insulin at the start of a competition day" : { }, @@ -29398,231 +27805,112 @@ "no change needed" : { }, - "No connected devices, or failure during device connection" : { - "comment" : "The error message displayed for device connection errors.", + "No Delivery" : { + + }, + "No glucose data for this period." : { + "comment" : "Statistics empty state" + }, + "No Maximum Bolus Configured" : { + "comment" : "Alert title for a missing maximum bolus setting error", "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "لا يوجد أجهزة متصلة, أو يوجد خطأ أثناء الاتصال" - } - }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Ingen tilsluttede enheder eller fejl under forbindelse til enhed" + "value" : "Ingen maksimal bolus konfigureret" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Keine verbundenen Geräte oder Fehler während der Geräteverbindung" + "value" : "Kein maximaler Bolus konfiguriert" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "No hay dispositivos conectados o falla durante conexión de dispositivo" + "value" : "No hay bolo máximo configurado" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Ei yhdistettyjä laitteita tai häiriö laiteyhteydessä" + "value" : "Maksimibolusta ei ole määritetty" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Pas d'appareil connecté, ou échec durant la connexion à l'appareil" + "value" : "Aucun Bolus Maximum configuré" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "אין מכשירים מחוברים, או שישנה שגיאת התחברות." + "value" : "לא הוגדר בולוס מקסימלי" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Nessun dispositivo connesso o mancanza di segnale durante la connessione del dispositivo" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "機器が未接続、または接続に問題" + "value" : "Nessun bolo massimo configurato" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Ingen tilkoblede enheter, eller feil under enhetstilkobling" + "value" : "Ingen maksimal bolus er konfigurert" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Geen aangesloten apparaten of storingen tijdens het verbinden met het apparaat" + "value" : "Geen maximale bolus geconfigureerd" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Brak podłączonych urządzeń lub awaria podczas połączenia urządzenia" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nenhum dispositivo conectado ou falha durante a conexão" + "value" : "Nie skonfigurowano maksymalnego bolusa" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Nu este conectat niciun dispozitiv sau s-a produs o eroare la conectare" + "value" : "Bolusul maxim nu este setat" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Устройства не сопряжены или произошла ошибка во время сопряжения" + "value" : "Не установлен максимальный болюс" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Ingen ansluten enhet, eller fel vid anslutning till enhet" + "value" : "Ingen maximal bolus har blivit inställd" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Bağlı cihaz yok veya cihaz bağlantısı sırasında hata" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Không có thiết bị nào được kết nối, hoặc lỗi trong quá trình kết nối" + "value" : "Yapılandırılmış Maksimum Bolus Yok" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "没有连接的设备,或设备连接期间发生故障" + "value" : "没有设置最大推注量" } } } }, - "No Delivery" : { + "No performance history available yet" : { }, - "No glucose data for this period." : { - "comment" : "Statistics empty state" - }, - "No Maximum Bolus Configured" : { - "comment" : "Alert title for a missing maximum bolus setting error", - "localizations" : { - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ingen maksimal bolus konfigureret" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kein maximaler Bolus konfiguriert" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "No hay bolo máximo configurado" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Maksimibolusta ei ole määritetty" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aucun Bolus Maximum configuré" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "לא הוגדר בולוס מקסימלי" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nessun bolo massimo configurato" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ingen maksimal bolus er konfigurert" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Geen maximale bolus geconfigureerd" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nie skonfigurowano maksymalnego bolusa" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bolusul maxim nu este setat" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Не установлен максимальный болюс" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ingen maximal bolus har blivit inställd" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Yapılandırılmış Maksimum Bolus Yok" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "没有设置最大推注量" - } - } - } - }, - "No performance history available yet" : { - - }, - "No Pump Configured" : { - "comment" : "Alert title for a missing pump error", + "No Pump Configured" : { + "comment" : "Alert title for a missing pump error", "localizations" : { "da" : { "stringUnit" : { @@ -30918,7 +29206,7 @@ } }, "OK" : { - "comment" : "Alert acknowledgment OK button\nCritical Alert permissions disabled alert button\nDefault action for alert when alert acknowledgment fails\nLabel for acknowledging the preset has been active for 24 hours\nNotifications permissions disabled alert button\nText for ok action on notification of upcoming TestFlight expiration\nText for ok action on notification of upcoming profile expiration\nThe title of the notification action to acknowledge a device alert", + "comment" : "Alert acknowledgment OK button\nCritical Alert permissions disabled alert button\nDefault action for alert when alert acknowledgment fails\nDismiss the pod-settling notice\nLabel for acknowledging the preset has been active for 24 hours\nNotifications permissions disabled alert button\nText for ok action on notification of upcoming TestFlight expiration\nText for ok action on notification of upcoming profile expiration\nThe title of the notification action to acknowledge a device alert", "localizations" : { "ar" : { "stringUnit" : { @@ -32977,125 +31265,6 @@ } } }, - "Pump data is %1$@ old" : { - "comment" : "The error message when pump data is too old to be used. (1: pump data age in minutes)", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "بيانات المضخة منذ %1$@ " - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pumpedata er %1$@ gammel" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pumpendaten sind %1$@ alt" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Los datos de la microinfusora son %1$@ viejos" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pumpputieto on %1$@ vanha" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Les données de la pompe remontent à %1$@" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "מידע משאבה: לפני %1$@" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "I dati forniti dalla pompa sono di %1$@ fa" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "ポンプデータが %1$@前のものです" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pumpedata er %1$@ gammel" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pompgegevens zijn %1$@ oud" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dane z pompy są nieaktualne od %1$@" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dados da bomba são de %1$@ atrás" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Datele din pompă sunt vechi de %1$@" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Данные помпы от %1$@ " - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pumpvärdena är %1$@ gamla" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pompa verisi %1$@ eski" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dữ liệu bơm %1$@ là cũ" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "胰岛素泵数据%1$@分钟未更新" - } - } - } - }, "Pump Event" : { "comment" : "The title of the screen displaying a pump event", "localizations" : { @@ -33273,396 +31442,191 @@ } } }, - "Pump Inoperable. Automatic dosing is disabled." : { - "comment" : "The error message displayed for LoopError.pumpInoperable errors." - }, - "Pump Manager" : { - "comment" : "Details for configuration error when pump manager is missing", + "Pump Reservoir Empty" : { + "comment" : "The notification title for an empty pump reservoir", + "extractionState" : "manual", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "إدارة المضخة" + "value" : "خزان المضخة منتهي" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Pumpemanager" + "value" : "Pumpereservoir tomt" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Pumpenmanager" + "value" : "Pumpenreservoir leer" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pump Reservoir Empty" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Administratión de Microinfusora" + "value" : "Reserva de bomba vacía" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Pumpun hallinta" + "value" : "Pumpun säiliö tyhjä" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Gestionnaire de pompe" + "value" : "Réservoir de la pompe vide" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "מנהל משאבה" + "value" : "מאגר משאבה ריק" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Pump Manager" + "value" : "Serbatoio della pompa vuoto" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "ポンプ設定" + "value" : "ポンプのリザーバが空です" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Pumpe manager" + "value" : "Pumpereservoaret tomt" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Pomp manager" + "value" : "Pompreservoir leeg" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Zarządzanie Pompą" + "value" : "Zbiorniczek w pompie jest pusty" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Gerenciamento da Bomba" + "value" : "Reservatório da Bomba Vazio" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Manager pompă" + "value" : "Rezervorul pompei este gol" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Менеджер помпо" + "value" : "Резервуар помпы пуст" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Pumphantering" + "value" : "Pumpreservoaren är tom" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Pompa Yöneticisi" + "value" : "Pompa Rezervuarı Boş" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Trình quản lý bơm" + "value" : "Ngăn chứa hết insulin" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "泵管理" + "value" : "储药器药量已用完" } } } }, - "Pump Manager Error: %1$@" : { - "comment" : "The error message displayed for pump manager errors. (1: pump manager error)", + "Pump Reservoir Low" : { + "comment" : "The notification title for a low pump reservoir", + "extractionState" : "manual", "localizations" : { - "da" : { + "ar" : { "stringUnit" : { "state" : "translated", - "value" : "Fejl i pumpemanager: %1$@" + "value" : "خزان المضخة منخفض" } }, - "de" : { + "da" : { "stringUnit" : { "state" : "translated", - "value" : "Pumpen-Fehler: %1$@" + "value" : "Pumpereservoir lavt" } }, - "es" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "Error del administrador de bomba: %1$@" + "value" : "Pumpenreservoir niedrig" } }, - "fr" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Erreur du gestionnaire de pompe : %1$@" + "value" : "Pump Reservoir Low" } }, - "he" : { + "es" : { "stringUnit" : { "state" : "translated", - "value" : "שגיאה במנהל שאבה: %1$@" + "value" : "Reserva de bomba baja" } }, - "it" : { + "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Errore Pump Manager: %1$@" + "value" : "Pumpun säiliö vähissä" } }, - "nb" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Pump Manager-feil: %1$@" + "value" : "Réservoir de la pompe bas" } }, - "nl" : { + "he" : { "stringUnit" : { "state" : "translated", - "value" : "Pompmanager fout: %1$@" + "value" : "מאגר משאבה נמוך" } }, - "pl" : { + "it" : { "stringUnit" : { "state" : "translated", - "value" : "Błąd menedżera pompy: %1$@" + "value" : "Serbatoio della pompa scarico" } }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Eroare de gestionare a pompei: %1$@" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ошибка менеджера помп: %1$@" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pompa Yöneticisi Hatası: %1$@" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "泵管理器错误:%1$@" - } - } - } - }, - "Pump Reservoir Empty" : { - "comment" : "The notification title for an empty pump reservoir", - "extractionState" : "manual", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "خزان المضخة منتهي" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pumpereservoir tomt" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pumpenreservoir leer" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pump Reservoir Empty" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reserva de bomba vacía" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pumpun säiliö tyhjä" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Réservoir de la pompe vide" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "מאגר משאבה ריק" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Serbatoio della pompa vuoto" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "ポンプのリザーバが空です" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pumpereservoaret tomt" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pompreservoir leeg" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zbiorniczek w pompie jest pusty" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reservatório da Bomba Vazio" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rezervorul pompei este gol" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Резервуар помпы пуст" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pumpreservoaren är tom" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pompa Rezervuarı Boş" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ngăn chứa hết insulin" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "储药器药量已用完" - } - } - } - }, - "Pump Reservoir Low" : { - "comment" : "The notification title for a low pump reservoir", - "extractionState" : "manual", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "خزان المضخة منخفض" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pumpereservoir lavt" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pumpenreservoir niedrig" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pump Reservoir Low" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reserva de bomba baja" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pumpun säiliö vähissä" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Réservoir de la pompe bas" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "מאגר משאבה נמוך" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Serbatoio della pompa scarico" - } - }, - "ja" : { + "ja" : { "stringUnit" : { "state" : "translated", "value" : "ポンプのリザーバが低です" @@ -33856,101 +31820,6 @@ } } }, - "Pump Suspended. Automatic dosing is disabled." : { - "comment" : "The error message displayed for LoopError.pumpSuspended errors.", - "localizations" : { - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pumpen er suspenderet. Automatisk dosering er deaktiveret." - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pumpe unterbrochen. Die automatische Dosierung ist deaktiviert." - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Microinfusora suspendida. La dosificación automática está desactivada." - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pumppu pysäytetty. Automaattinen annostelu ei ole käytössä." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pompe suspendue. Le dosage automatique est désactivé." - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "משאבה מושהית. מינון אוטומטי מושבת." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pompa sospesa. Il dosaggio automatico è disattivato." - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pumpe suspendert. Automatisk dosing er deaktivert." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pomp onderbroken. Automatisch doseren is uitgeschakeld." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pompa zawieszona. Automatyczne dozowanie jest wyłączone." - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pompă suspendată. Administrarea automată este dezactivată." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Помпа приостановлена. Авто болюсы и ВБС отключены." - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pump pausad. Automatisk dosering är inaktiverad." - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pompa Askıya Alındı. Otomatik dozlama devre dışı bırakıldı." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "泵已暂停。自动给药功能已关闭。" - } - } - } - }, "QUANTITY_VALUE_AND_UNIT" : { "comment" : "Format string for combining localized numeric value and unit. (1: numeric value)(2: unit)", "extractionState" : "extracted_with_value", @@ -34105,357 +31974,238 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Rapid-Acting – Adults" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Acción Rápida — Adultos" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nopeavaikutteinen – aikuiset" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Action rapide - Adulte" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "אינסולין מהיר - מבוגרים" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulina ultrarapida – Adulti" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "超速攻型 - 大人" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hurtigvirkende – voksne" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Snelwerkend - volwassenen" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Szybko działające – dorośli" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ação-Rápida – Adultos" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Acțiune rapidă – Adulți" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Быстродействующий - взрослые" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Snabbverkande – vuxna" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hızlı Etkili – Yetişkinler" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Thuốc tác động nhanh cho người lớn" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "速效胰岛素 - 成人模型" - } - } - } - }, - "Rapid-Acting – Children" : { - "comment" : "Title of insulin model preset", - "extractionState" : "manual", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "التأثيرالسريع - أطفال" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hurtigt-virkende (Rapid) – Børn" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Schnell wirkend – Kinder" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rapid-Acting – Children" + "value" : "Rapid-Acting – Adults" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Acción Rápida — Niños" + "value" : "Acción Rápida — Adultos" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Nopeavaikutteinen – lapset" + "value" : "Nopeavaikutteinen – aikuiset" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Action rapide - Enfant" + "value" : "Action rapide - Adulte" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "אינסולין מהיר - ילדים" + "value" : "אינסולין מהיר - מבוגרים" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Insulina ultrarapida – Bambini" + "value" : "Insulina ultrarapida – Adulti" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "超速攻型 - 子供" + "value" : "超速攻型 - 大人" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Hurtigvirkende – barn" + "value" : "Hurtigvirkende – voksne" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Snelwerkend - kinderen" + "value" : "Snelwerkend - volwassenen" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Szybko działająca – dzieci" + "value" : "Szybko działające – dorośli" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Ação-Rapida – Crianças" + "value" : "Ação-Rápida – Adultos" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Acțiune rapidă - Copii" + "value" : "Acțiune rapidă – Adulți" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Быстродействующий - дети" + "value" : "Быстродействующий - взрослые" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Snabbverkande – barn" + "value" : "Snabbverkande – vuxna" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Hızlı Etkili – Çocuklar" + "value" : "Hızlı Etkili – Yetişkinler" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Thuốc tác động nhanh cho trẻ em" + "value" : "Thuốc tác động nhanh cho người lớn" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "速效胰岛素 - 儿童模型" + "value" : "速效胰岛素 - 成人模型" } } } }, - "Reading & Changing Access" : { - "comment" : "Health access section header for reading and changing access" - }, - "Recognizing how hard you feel you're working during exercise can help you understand its impact on your glucose levels." : { - - }, - "Recommendation expired: %1$@ old" : { - "comment" : "The error message when a recommendation has expired. (1: age of recommendation in minutes)", + "Rapid-Acting – Children" : { + "comment" : "Title of insulin model preset", + "extractionState" : "manual", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "انتهت صلاحية التوصية منذ: %1$@ " + "value" : "التأثيرالسريع - أطفال" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Forslag udløbet: %1$@ gamle" + "value" : "Hurtigt-virkende (Rapid) – Børn" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Empfehlung abgelaufen: %1$@ alt" + "value" : "Schnell wirkend – Kinder" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rapid-Acting – Children" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Recomendación expiró hace: %1$@" + "value" : "Acción Rápida — Niños" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Suositus vanhentunut: %1$@ vanha" + "value" : "Nopeavaikutteinen – lapset" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Recommandation expirée, remonte à %1$@" + "value" : "Action rapide - Enfant" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "המלצה פגה: לפני %1$@" + "value" : "אינסולין מהיר - ילדים" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "La raccomandazione è scaduta: %1$@ fa" + "value" : "Insulina ultrarapida – Bambini" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "推奨が %1$@ 経過したため失効" + "value" : "超速攻型 - 子供" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Anbefaling utløpt: %1$@ gammelt" + "value" : "Hurtigvirkende – barn" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Aanbeveling verlopen: %1$@ oud" + "value" : "Snelwerkend - kinderen" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Rekomendacja nieaktualna od %1$@" + "value" : "Szybko działająca – dzieci" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Recomendação expirou: %1$@ atrás" + "value" : "Ação-Rapida – Crianças" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Recomandare expirată: acum %1$@" + "value" : "Acțiune rapidă - Copii" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Рекомнендация истекла : от %1$@" + "value" : "Быстродействующий - дети" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Rekommendationen gick ut för %1$@ sedan" + "value" : "Snabbverkande – barn" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Önerinin süresi doldu: %1$@ eski" + "value" : "Hızlı Etkili – Çocuklar" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Khuyến cáo hết hạn: %1$@ phút" + "value" : "Thuốc tác động nhanh cho trẻ em" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@分钟前推荐剂量已过期" + "value" : "速效胰岛素 - 儿童模型" } } } + }, + "Reading & Changing Access" : { + "comment" : "Health access section header for reading and changing access" + }, + "Recognizing how hard you feel you're working during exercise can help you understand its impact on your glucose levels." : { + }, "Recommended Basal" : { "comment" : "The title of the cell displaying a recommended temp basal value", @@ -37560,132 +35310,6 @@ } } }, - "Suspend Threshold" : { - "comment" : "The title text in settings", - "extractionState" : "manual", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "قيمة التعليق" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pause grænseværdi" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Grenzwert für Hypo-Abschaltung" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Suspend Threshold" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nivel de Suspensión" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pysäytysraja" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Seuil de suspension" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "סף השהייה" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Soglia di sospensione" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "一時停止値" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Terskel for utsettelse" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Onderbrekingsdrempel" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Próg zawieszenia pompy" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Limite de Suspenção" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Limită suspendare" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Порог приостановки" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tröskelvärde" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Eşiği Askıya Al" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ngưỡng Tạm dừng" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "暂停阈值" - } - } - } - }, "Suspension of Insulin Delivery" : { "comment" : "Title of the prediction input effect for suspension of insulin delivery", "localizations" : { @@ -41186,101 +38810,6 @@ } } }, - "Unknown Error: %1$@" : { - "comment" : "The error message displayed for unknown LoopError errors. (1: unknown error)", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "خطأ غير معروف" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Neznámá chyba: %1$@" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ukendt fejl: %1$@" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Unbekannter Fehler: %1$@" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Error desconocido: %1$@" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Erreur inconnue : %1$@" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "תקלה לא ידועה %1$@" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Errore sconosciuto: %1$@" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ukjent feil: %1$@" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Onbekende fout: %1$@" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nieznany błąd: %1$@" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Eroare necunoscută: %1$@" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Неизвестная ошибка: %1$@" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bilinmeyen Hata: %1$@" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "未知错误:%1$@" - } - } - } - }, "Unknown preset: %1$@" : { "comment" : "Override error description: unknown preset (1: preset name).", "localizations" : { diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index a9df54061f..87b01bed5d 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -821,14 +821,6 @@ extension DeviceDataManager { } } } - - var pumpManagerStatus: PumpManagerStatus? { - return pumpManager?.status - } - - var cgmManagerStatus: CGMManagerStatus? { - return cgmManager?.cgmManagerStatus - } func glucoseDisplay(for glucose: GlucoseSampleValue?) -> GlucoseDisplayable? { guard let glucose = glucose else { @@ -1458,40 +1450,11 @@ extension DeviceDataManager: DeviceSupportDelegate { } } -extension DeviceDataManager: DeliveryDelegate { - var isPumpConfigured: Bool { - return pumpManager != nil - } - - func roundBasalRate(unitsPerHour: Double) -> Double { - guard let pumpManager = pumpManager else { - return unitsPerHour - } - - return pumpManager.roundToSupportedBasalRate(unitsPerHour: unitsPerHour) - } - - func roundBolusVolume(units: Double) -> Double { - guard let pumpManager = pumpManager else { - return units - } - - return pumpManager.roundToSupportedBolusVolume(units: units) - } - - var pumpInsulinType: InsulinType? { - return pumpManager?.status.insulinType - } - - var isSuspended: Bool { - return pumpManager?.status.basalDeliveryState?.isSuspended ?? false - } - - var isManualTempBasalRunning: Bool { - guard case .tempBasal(let dose) = pumpManager?.status.basalDeliveryState else { - return false - } - return dose.automatic == false && dose.endDate > Date() +extension DeviceDataManager: PumpManagerDeliveryDelegate { + func send(bolus: Double?, tempBasal: TempBasalRecommendation?, decisionId: UUID?, to pumpManager: PumpManager) async throws { + log.default("Enacting dose: %{public}@", String(describing: (bolus, tempBasal))) + + try await doseEnactor.enact(decisionId: decisionId, bolus: bolus, tempBasal: tempBasal, with: pumpManager) } var isPumpInoperable: Bool { @@ -1500,24 +1463,6 @@ extension DeviceDataManager: DeliveryDelegate { } return basalDeliveryState == .pumpInoperable } - - func enact(bolus: Double?, tempBasal: TempBasalRecommendation?, decisionId: UUID?) async throws { - guard let pumpManager = pumpManager else { - throw LoopError.configurationError(.pumpManager) - } - - guard !pumpManager.status.deliveryIsUncertain else { - throw LoopError.connectionError - } - - log.default("Enacting dose: %{public}@", String(describing: (bolus, tempBasal))) - - try await doseEnactor.enact(decisionId: decisionId, bolus: bolus, tempBasal: tempBasal, with: pumpManager) - } - - var basalDeliveryState: PumpManagerStatus.BasalDeliveryState? { - return pumpManager?.status.basalDeliveryState - } } extension DeviceDataManager: DeviceStatusProvider {} diff --git a/Loop/Managers/Live Activity/ChartAxisGenerator.swift b/Loop/Managers/Live Activity/ChartAxisGenerator.swift index b2db2973e2..2868afaa3b 100644 --- a/Loop/Managers/Live Activity/ChartAxisGenerator.swift +++ b/Loop/Managers/Live Activity/ChartAxisGenerator.swift @@ -11,6 +11,7 @@ import HealthKit import LoopKit import SwiftCharts import UIKit +import LoopCore struct ChartAxisGenerator { private static let yAxisStepSizeMGDLOverride: Double? = FeatureFlags.predictedGlucoseChartClampEnabled ? 40 : nil diff --git a/Loop/Managers/LoopDataManager+CarbAbsorption.swift b/Loop/Managers/LoopDataManager+CarbAbsorption.swift index 3aee4d581a..04862bdc89 100644 --- a/Loop/Managers/LoopDataManager+CarbAbsorption.swift +++ b/Loop/Managers/LoopDataManager+CarbAbsorption.swift @@ -8,6 +8,7 @@ import Foundation import LoopKit +import LoopCore import LoopAlgorithm struct CarbAbsorptionReview { diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index 8a60a1e113..e47ae5afc6 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -15,74 +15,6 @@ import LoopCore import WidgetKit -struct AlgorithmDisplayState { - var input: StoredDataAlgorithmInput? - var output: AlgorithmOutput? - - /// The ISF multiplier slow retrospective correction applied on the last run, 1 when it is off. - var sensitivityMultiplier: Double { - output?.slowRetrospectiveCorrection?.multiplier ?? 1 - } - - var activeInsulin: InsulinValue? { - guard let input, let value = output?.activeInsulin else { - return nil - } - return InsulinValue(startDate: input.predictionStart, value: value) - } - - var activeCarbs: CarbValue? { - guard let input, let value = output?.activeCarbs else { - return nil - } - return CarbValue(startDate: input.predictionStart, value: value) - } - - var asTuple: (algoInput: StoredDataAlgorithmInput?, algoOutput: AlgorithmOutput?) { - return (algoInput: input, algoOutput: output) - } -} - -protocol DeliveryDelegate: AnyObject { - var isSuspended: Bool { get } - var isManualTempBasalRunning: Bool { get } - var pumpInsulinType: InsulinType? { get } - var basalDeliveryState: PumpManagerStatus.BasalDeliveryState? { get } - var isPumpConfigured: Bool { get } - var pumpManagerStatus: PumpManagerStatus? { get } - var pumpStatusHighlight: DeviceStatusHighlight? { get } - var cgmManagerStatus: CGMManagerStatus? { get } - - func enact(bolus: Double?, tempBasal: TempBasalRecommendation?, decisionId: UUID?) async throws - func enactBolus(units: Double, decisionId: UUID?, activationType: BolusActivationType) async throws - func roundBasalRate(unitsPerHour: Double) -> Double - func roundBolusVolume(units: Double) -> Double -} - -extension PumpManagerStatus.BasalDeliveryState { - var currentTempBasal: DoseEntry? { - switch self { - case .tempBasal(let dose): - return dose - default: - return nil - } - } - - func currentBasalRate(currentScheduledBasalRate: Double) -> Double? { - switch self { - case .tempBasal(let dose): - return dose.unitsPerHour - case .suspended: - return 0 - case .pumpInoperable: - return nil - default: - return currentScheduledBasalRate - } - } -} - protocol DosingManagerDelegate { func didMakeDosingDecision(_ decision: StoredDosingDecision) } @@ -120,6 +52,13 @@ final class LoopDataManager: ObservableObject { } @Published private(set) var lastLoopCompleted: Date? + + /// Display only: at reclaim the watch's last cycle seeds the phone's loop-recency clock. + /// Forward-only; dosing never reads it. + func seedLastLoopCompleted(fromWatch date: Date) { + guard (lastLoopCompleted ?? .distantPast) < date else { return } + lastLoopCompleted = date + } @Published private(set) var publishedMostRecentGlucoseDataDate: Date? @Published private(set) var publishedMostRecentPumpDataDate: Date? @Published private(set) var lastManualBolus: LastManualBolus? @@ -144,6 +83,11 @@ final class LoopDataManager: ObservableObject { private var now: Date { TestingDate.currentTestingDate() } + /// The dosing loop both apps run (`LoopCycle.swift`); this manager is its host on the phone. + private(set) lazy var cycle = LoopCycle(doseStore: doseStore, carbStore: carbStore, glucoseStore: glucoseStore, + dosingDecisionStore: dosingDecisionStore, settingsProvider: settingsProvider, + host: self, now: { TestingDate.currentTestingDate() }) + // References to registered notification center observers private var notificationObservers: [Any] = [] private var overrideIntentObserver: NSKeyValueObservation? = nil @@ -169,9 +113,8 @@ final class LoopDataManager: ObservableObject { var carbAbsorptionModel: CarbAbsorptionModel - private var lastManualBolusRecommendation: ManualBolusRecommendation? - private var volatilityBaseline: (value: Double?, date: Date)? + private(set) var volatilityBaseline: (value: Double?, date: Date)? private(set) var dosingStrategySelectionEnabled: Bool @@ -371,221 +314,12 @@ final class LoopDataManager: ObservableObject { ensureCarbCoverageStart: Date? = nil, projectOngoingDoses: Bool = false ) async throws -> StoredDataAlgorithmInput { - let experiments = enabledAlgorithmExperiments - - // Slow retrospective correction keeps no state, so it reads its whole window of glucose on every run, - // with the carbs and doses that explain it. - let slowRetrospectiveCorrectionHistory: TimeInterval? = experiments.contains(.slowRetrospectiveCorrection) ? .hours(72) : nil - - // Need to fetch doses back as far as t - (DIA + DCA) for Dynamic carbs - var dosesInputHistory = CarbMath.maximumAbsorptionTimeInterval + InsulinMath.defaultInsulinActivityDuration - if let slowRetrospectiveCorrectionHistory { - dosesInputHistory = max(dosesInputHistory, slowRetrospectiveCorrectionHistory + InsulinMath.defaultInsulinActivityDuration) - } - - let baseTime = baseTime ?? now - - var dosesStart = baseTime.addingTimeInterval(-dosesInputHistory) - - // Ensure dosing data goes back before ensureDosingCoverageStart, if specified - if let ensureDosingCoverageStart { - dosesStart = min(ensureDosingCoverageStart, dosesStart) - } - - // When projectOngoingDoses is true (display path), pass end:nil so DoseStore - // extends a mutable suspend to its insulin-activity-duration fallback. Doses - // already in flight (e.g. a manual temp basal) keep their actual endDate - // either way; only the suspend extension is gated on this flag. - let doses = try await doseStore.getNormalizedDoseEntries( - start: dosesStart, - end: projectOngoingDoses ? nil : baseTime - ) - - // Doses that were included because they cover dosesStart might have a start time earlier than dosesStart - // This moves the start time back to ensure basal covers - dosesStart = min(dosesStart, doses.map { $0.startDate }.min() ?? dosesStart) - - // Doses with a start time before baseTime might still end after baseTime - let dosesEnd = max(baseTime, doses.map { $0.endDate }.max() ?? baseTime) - - let rawBasal = try await settingsProvider.getBasalHistory(startDate: dosesStart, endDate: dosesEnd) - - guard !rawBasal.isEmpty else { - throw LoopError.configurationError(.basalRateSchedule) - } - - // Collapse contiguous same-rate basal entries. getBasalHistory projects the - // daily BasalRateSchedule onto absolute time and splits at every local - // midnight even when the rate doesn't change; InsulinDose.annotated(with:) - // then splits the suspend (or any long basal-typed dose) at each of those - // boundaries, and the continuous-delivery IOB integrator doesn't join the - // resulting sub-doses perfectly across the boundary -- visible as a small - // bump in Active Insulin at midnight even with a single-rate schedule. - let basal: [AbsoluteScheduleValue] = rawBasal.reduce(into: []) { acc, entry in - if let last = acc.last, last.value == entry.value, last.endDate == entry.startDate { - acc[acc.count - 1] = AbsoluteScheduleValue(startDate: last.startDate, endDate: entry.endDate, value: last.value) - } else { - acc.append(entry) - } - } - - let forecastEndTime = baseTime.addingTimeInterval(InsulinMath.defaultInsulinActivityDuration).dateCeiledToTimeInterval(GlucoseMath.defaultDelta) - - var glucoseStart = baseTime.addingTimeInterval(LoopConstants.maxCarbEntryPastTime + .minutes(-1)) // additional minute to handle difference in seconds between carb entry and carb ratio - var carbsStart = glucoseStart - if let slowRetrospectiveCorrectionHistory { - glucoseStart = baseTime.addingTimeInterval(-slowRetrospectiveCorrectionHistory) - carbsStart = glucoseStart.addingTimeInterval(-CarbMath.maximumAbsorptionTimeInterval + .minutes(-1)) - } - // A pending carb entry can outlive this rolling window while the bolus screen is open. - // Cover it with schedule history without expanding the stored-carb or glucose queries. - let carbScheduleStart = min(carbsStart, ensureCarbCoverageStart ?? carbsStart) - - // Include future carbs in query, but filter out ones entered after basetime. The filtering is only applicable when running in a retrospective situation. - let carbEntries = try await carbStore.getCarbEntries( - start: carbsStart, - end: forecastEndTime - ).filter { - $0.userCreatedDate ?? $0.startDate < baseTime - } - - let carbRatio = try await settingsProvider.getCarbRatioHistory( - startDate: carbScheduleStart, - endDate: forecastEndTime - ) - - guard !carbRatio.isEmpty else { - throw LoopError.configurationError(.carbRatioSchedule) - } - - let glucose = try await glucoseStore.getGlucoseSamples(start: glucoseStart, end: baseTime) - - let dosesWithModel = doses.map { $0.simpleDose(with: insulinModel(for: $0.insulinType)) } - - let recommendationInsulinModel = insulinModel(for: deliveryDelegate?.pumpInsulinType ?? .novolog) - - let recommendationEffectInterval = DateInterval( - start: baseTime, - duration: recommendationInsulinModel.effectDuration - ) - let neededSensitivityTimeline = Algorithm.timelineIntervalForSensitivity( - doses: dosesWithModel, - glucoseHistoryStart: glucose.first?.startDate ?? baseTime, - recommendationEffectInterval: recommendationEffectInterval - ) - - // Carb entries (including a pending manual-bolus entry) can extend back to carbScheduleStart, and - // CarbMath.map(to:) preconditionFailures if the ISF/carb-ratio timelines don't cover every - // carb entry's start date. timelineIntervalForSensitivity derives its window from dose and - // glucose history only — which can be more recent than carbsStart (e.g. after a CGM gap) — - // so extend the ISF (and override) window back to cover every entry, matching carbRatio. - let sensitivityStart = min(neededSensitivityTimeline.start, carbScheduleStart) - - let sensitivity = try await settingsProvider.getInsulinSensitivityHistory( - startDate: sensitivityStart, - endDate: neededSensitivityTimeline.end - ) - - let dosingLimits = try await settingsProvider.getDosingLimits(at: baseTime) - - guard let maxBolus = dosingLimits.maxBolus else { - throw LoopError.configurationError(.maximumBolus) - } - - guard let maxBasalRate = dosingLimits.maxBasalRate else { - throw LoopError.configurationError(.maximumBasalRatePerHour) - } - - var overrides = temporaryPresetsManager.presetHistory.getOverrideHistory(startDate: sensitivityStart, endDate: forecastEndTime) - - // For recommendation, we should consider preMeal override to be ending at time of dose - if presumePresetEndingNow, - let activeOverride = temporaryPresetsManager.activeOverride, - let index = overrides.lastIndex(of: activeOverride) { - overrides[index].scheduledEndDate = baseTime - } - - guard !sensitivity.isEmpty else { - throw LoopError.configurationError(.insulinSensitivitySchedule) - } - - let sensitivityWithOverrides = overrides.applySensitivity(over: sensitivity) - - guard !basal.isEmpty else { - throw LoopError.configurationError(.basalRateSchedule) - } - let basalWithOverrides = overrides.applyBasal(over: basal) - - guard !carbRatio.isEmpty else { - throw LoopError.configurationError(.carbRatioSchedule) - } - let carbRatioWithOverrides = overrides.applyCarbRatio(over: carbRatio) - - - var target: [AbsoluteScheduleValue>] - - guard var suspendThreshold = dosingLimits.suspendThreshold else { - throw LoopError.configurationError(.suspendThreshold) - } - - // If we have an active override, and it's not a preMeal override that should be disabled, - // or ended for other reasons (like comparing effects without preset), then override the - // target for the entire forecast. - if let activeOverride = temporaryPresetsManager.activeOverride, - !presumePresetEndingNow - { - guard let schedule = settingsProvider.settings.glucoseTargetRangeSchedule else - { - throw LoopError.configurationError(.glucoseTargetRangeSchedule) - } - let scheduledRange = schedule.quantityRange(at: baseTime) - let overriddenTargetRange = activeOverride.effectiveCorrectionRangeDuring(scheduledRange: scheduledRange) - target = [ - AbsoluteScheduleValue( - startDate: baseTime, - endDate: forecastEndTime, - value: overriddenTargetRange - ) - ] - - if activeOverride.veryHighInsulinNeeds { - suspendThreshold = max(TemporaryScheduleOverride.highInsulinNeedsMitigationCorrectionRangeLimit, suspendThreshold) - } - - } else { - target = try await settingsProvider.getTargetRangeHistory(startDate: baseTime, endDate: forecastEndTime) - } - - guard !target.isEmpty else { - throw LoopError.configurationError(.glucoseTargetRangeSchedule) - } - - - let volatilityBaseline = experiments.contains(.guardAndRunway) ? await currentVolatilityBaseline(at: baseTime) : nil + try await cycle.fetchData(for: baseTime, presumePresetEndingNow: presumePresetEndingNow, + ensureDosingCoverageStart: ensureDosingCoverageStart, ensureCarbCoverageStart: ensureCarbCoverageStart, + projectOngoingDoses: projectOngoingDoses) + } - return StoredDataAlgorithmInput( - glucoseHistory: glucose, - doses: dosesWithModel, - carbEntries: carbEntries, - predictionStart: baseTime, - basal: basalWithOverrides.algorithmSchedule(), - sensitivityMgdlPerUnit: sensitivityWithOverrides.algorithmMgdlSchedule(), - carbRatio: carbRatioWithOverrides.algorithmSchedule(), - targetMgdl: target.algorithmMgdlRanges(), - suspendThresholdMgdl: suspendThreshold.doubleValue(for: .milligramsPerDeciliter), - maxBolus: maxBolus, - maxBasalRate: maxBasalRate, - useIntegralRetrospectiveCorrection: false, - includePositiveVelocityAndRC: true, - carbAbsorptionModel: carbAbsorptionModel, - recommendationInsulinModel: AlgorithmInsulinModel(recommendationInsulinModel), - recommendationType: .manualBolus, - automaticBolusApplicationFactor: nil, - experiments: experiments, - volatilityBaseline: volatilityBaseline) - } - - private var enabledAlgorithmExperiments: Set { + var enabledAlgorithmExperiments: Set { var experiments: Set = [] if UserDefaults.standard.integralRetrospectiveCorrectionEnabled { experiments.insert(.integralRetrospectiveCorrection) @@ -609,15 +343,10 @@ final class LoopDataManager: ObservableObject { func updateDisplayState(forceStoreRemoteRecommendation: Bool = false) async { - var newState = AlgorithmDisplayState() - do { + let newState = await cycle.displayState() + if let input = newState.input { let lastManualBolusVisibilityWindowStartDate = now.addingTimeInterval(.days(-1)) - var input = try await fetchData(for: now, ensureDosingCoverageStart: lastManualBolusVisibilityWindowStartDate, projectOngoingDoses: true) - input.recommendationType = .manualBolus - newState.input = input - newState.output = await runAlgorithm(input: input) - let lastStoredManualBolus = input.doses.last( where: { $0.startDate >= lastManualBolusVisibilityWindowStartDate && $0.deliveryType == .bolus && $0.automatic == false @@ -638,9 +367,6 @@ final class LoopDataManager: ObservableObject { } else if let lastManualBolus = self.lastManualBolus, lastManualBolus.startDate < recentlyEnactedCutoff { self.lastManualBolus = nil } - } catch { - let loopError = error as? LoopError ?? .unknownError(error) - logger.error("Error updating Loop state: %{public}@", String(describing: loopError)) } displayState = newState publishedMostRecentGlucoseDataDate = glucoseStore.latestGlucose?.startDate @@ -657,36 +383,22 @@ final class LoopDataManager: ObservableObject { await updateRemoteRecommendation(force: forceStoreRemoteRecommendation) } - private nonisolated func runAlgorithm(input: StoredDataAlgorithmInput) async -> AlgorithmOutput { - Algorithm.run(input: input) - } - /// Cancel the active temp basal if it was automatically issued func cancelActiveTempBasal(for reason: CancelActiveTempBasalReason) async throws { - guard case .tempBasal(let dose) = deliveryDelegate?.basalDeliveryState, (dose.automatic ?? true) else { return } - - logger.default("Cancelling active temp basal for reason: %{public}@", String(describing: reason)) - - let recommendation = AutomaticDoseRecommendation(basalAdjustment: .cancel, direction: .decrease) - - var dosingDecision = StoredDosingDecision(reason: reason.rawValue) - dosingDecision.settings = StoredDosingDecision.Settings(settingsProvider.settings) - dosingDecision.automaticDoseRecommendation = recommendation - + let cancelled: Bool do { - crashRecoveryManager.dosingStarted(dose: recommendation) - try await deliveryDelegate?.enact(bolus: recommendation.bolusUnits, tempBasal: recommendation.basalAdjustment, decisionId: dosingDecision.id) - self.crashRecoveryManager.dosingFinished() + cancelled = try await cycle.cancelActiveTempBasal( + for: reason, + dosingStarted: { self.crashRecoveryManager.dosingStarted(dose: $0) }, + dosingFinished: { self.crashRecoveryManager.dosingFinished() }) } catch { - dosingDecision.appendError(error as? LoopError ?? .unknownError(error)) if reason == .maximumBasalRateChanged { throw CancelTempBasalFailedMaximumBasalRateChangedError(reason: error) } else { throw error } } - - await dosingDecisionStore.storeDosingDecision(dosingDecision) + guard cancelled else { return } // DIY: refresh post-dose forecast and persist an "updateRemoteRecommendation" // decision so Nightscout sees the post-cancel state. @@ -696,136 +408,21 @@ final class LoopDataManager: ObservableObject { func loop() async { let loopBaseTime = now - var dosingDecision = StoredDosingDecision( - date: loopBaseTime, - reason: "loop", - settings: StoredDosingDecision.Settings(settingsProvider.settings) - ) - - do { - guard let deliveryDelegate else { - preconditionFailure("Unable to dose without dosing delegate.") - } - - logger.debug("Running Loop at %{public}@", String(describing: loopBaseTime)) - NotificationCenter.default.post(name: .LoopRunning, object: self) - - var input = try await fetchData(for: loopBaseTime) - - // Trim future basal - input.doses = input.doses.trimmed(to: loopBaseTime) - - var dosingStrategy: AutomaticDosingStrategy = .automaticBolus - - if dosingStrategySelectionEnabled { - dosingStrategy = settingsProvider.settings.automaticDosingStrategy - } - input.recommendationType = dosingStrategy.recommendationType - dosingDecision.algorithmExperiments = algorithmExperiments(for: dosingStrategy) - - guard let latestGlucose = input.glucoseHistory.last else { - throw LoopError.missingDataError(.glucose) - } - - guard loopBaseTime.timeIntervalSince(latestGlucose.startDate) <= LoopCoreConstants.inputDataRecencyInterval else { - throw LoopError.glucoseTooOld(date: latestGlucose.startDate) - } - - guard latestGlucose.startDate.timeIntervalSince(loopBaseTime) <= LoopCoreConstants.inputDataRecencyInterval else { - throw LoopError.invalidFutureGlucose(date: latestGlucose.startDate) - } - - guard loopBaseTime.timeIntervalSince(doseStore.lastAddedPumpData) <= LoopCoreConstants.inputDataRecencyInterval else { - throw LoopError.pumpDataTooOld(date: doseStore.lastAddedPumpData) - } - - var output = Algorithm.run(input: input) - dosingDecision.algorithmExperiments = algorithmExperiments(for: dosingStrategy, output: output) - - switch output.recommendationResult { - case .success(let recommendation): - // Round delivery amounts to pump supported amounts, - // And determine if a change in dosing should be made. - - let algoRecommendation = recommendation.automatic! - logger.default("Algorithm recommendation: %{public}@", String(describing: algoRecommendation)) - - var recommendationToEnact = algoRecommendation - // Round bolus recommendation based on pump bolus precision - if let bolus = algoRecommendation.bolusUnits, bolus > 0 { - recommendationToEnact.bolusUnits = deliveryDelegate.roundBolusVolume(units: bolus) - } - - var basal = algoRecommendation.basalAdjustment - - basal.unitsPerHour = deliveryDelegate.roundBasalRate(unitsPerHour: basal.unitsPerHour) - - let scheduledBasalRate = input.basal.closestPrior(to: loopBaseTime)!.value - let activeOverride = temporaryPresetsManager.presetHistory.activeOverride(at: loopBaseTime) - - let basalAdjustment = basal.loopKit.adjustForCurrentDelivery( - at: loopBaseTime, - neutralBasalRate: scheduledBasalRate, - currentTempBasal: deliveryDelegate.basalDeliveryState?.currentTempBasal, - continuationInterval: .minutes(11), - neutralBasalRateMatchesPump: activeOverride == nil - ) - - if let basalAdjustment { - recommendationToEnact.basalAdjustment = basalAdjustment.algorithm - } - - output.recommendationResult = .success(.init(automatic: recommendationToEnact)) - - if recommendationToEnact != algoRecommendation { - logger.default("Recommendation changed to: %{public}@", String(describing: recommendationToEnact)) - } - - dosingDecision.updateFrom(input: input, output: output) - - if self.settingsProvider.dosingEnabled { - if deliveryDelegate.basalDeliveryState == .pumpInoperable { - throw LoopError.pumpInoperable - } - - if deliveryDelegate.isSuspended { - throw LoopError.pumpSuspended - } - - if deliveryDelegate.isManualTempBasalRunning { - throw LoopError.manualTempBasalRunning - } - - logger.default("Enacting: %{public}@", String(describing: recommendationToEnact)) + logger.debug("Running Loop at %{public}@", String(describing: loopBaseTime)) + NotificationCenter.default.post(name: .LoopRunning, object: self) - try await deliveryDelegate.enact(bolus: recommendationToEnact.bolusUnits, tempBasal: basalAdjustment, decisionId: dosingDecision.id) + let outcome = await cycle.run(at: loopBaseTime) - logger.default("loop() completed successfully.") - lastLoopCompleted = now - let duration = lastLoopCompleted!.timeIntervalSince(loopBaseTime) - - dosingDecision.enactedTempBasal = basalAdjustment - dosingDecision.enactedBolusAmount = recommendationToEnact.bolusUnits - - analyticsServicesManager?.loopDidSucceed(duration) - } else { - self.logger.default("Not adjusting dosing during open loop.") - } - - await dosingDecisionStore.storeDosingDecision(dosingDecision) - NotificationCenter.default.post(name: .LoopCycleCompleted, object: self) - - case .failure(let error): - throw error - } - } catch { - logger.error("loop() did error: %{public}@", String(describing: error)) - let loopError = error as? LoopError ?? .unknownError(error) - dosingDecision.appendError(loopError) - await dosingDecisionStore.storeDosingDecision(dosingDecision) + if outcome.enacted { + logger.default("loop() completed successfully.") + lastLoopCompleted = now + let duration = lastLoopCompleted!.timeIntervalSince(loopBaseTime) + analyticsServicesManager?.loopDidSucceed(duration) + } + if let loopError = outcome.error { analyticsServicesManager?.loopDidError(error: loopError) - NotificationCenter.default.post(name: .LoopCycleCompleted, object: self) } + NotificationCenter.default.post(name: .LoopCycleCompleted, object: self) // DIY: refresh post-dose forecast and persist an "updateRemoteRecommendation" // decision (Nightscout's Loop pill + forecast source — paired with the just-stored @@ -840,42 +437,8 @@ final class LoopDataManager: ObservableObject { carbEntryChange: CarbEntryChange? = nil, truncatingActiveOverride: Bool = false ) async throws -> ManualBolusRecommendation? { - - var endingPremealOverride = false - - if carbEntryChange != nil, - let activeOverride = temporaryPresetsManager.activeOverride, - activeOverride.context == .preMeal - { - endingPremealOverride = true - } - - var input = try await self.fetchData( - for: now, - presumePresetEndingNow: truncatingActiveOverride || endingPremealOverride, - ensureCarbCoverageStart: carbEntryChange?.startDate - ) - .addingGlucoseSample(sample: manualGlucoseSample?.asStoredGlucoseSample) - .removingCarbEntry(carbEntry: carbEntryChange?.originalEntry) - .addingCarbEntry(carbEntry: carbEntryChange?.asStoredCarbEntry) - - input.includePositiveVelocityAndRC = usePositiveMomentumAndRCForManualBoluses - input.recommendationType = .manualBolus - - try input.validateCarbScheduleCoverage() - let output = Algorithm.run(input: input) - - - switch output.recommendationResult { - case .success(let prediction): - guard var manualBolusRecommendation = prediction.manual else { return nil } - if let roundedAmount = deliveryDelegate?.roundBolusVolume(units: manualBolusRecommendation.amount) { - manualBolusRecommendation.amount = roundedAmount - } - return manualBolusRecommendation.loopKit - case .failure(let error): - throw error - } + try await cycle.recommendManualBolus(manualGlucoseSample: manualGlucoseSample, carbEntryChange: carbEntryChange, + truncatingActiveOverride: truncatingActiveOverride) } public func totalDeliveredToday() async -> InsulinValue? @@ -927,60 +490,9 @@ final class LoopDataManager: ObservableObject { } func updateRemoteRecommendation(force: Bool = false) async { - if lastManualBolusRecommendation == nil { - lastManualBolusRecommendation = displayState.output?.recommendation?.manual?.loopKit - } - - let recommendationChanged = lastManualBolusRecommendation != displayState.output?.recommendation?.manual?.loopKit - - // DIY: post-dose "updateRemoteRecommendation" decisions are also Nightscout's - // Loop pill + forecast source (NightscoutService pairs them with the cached "loop" - // decision). Force-store after every Loop cycle and temp basal cancel so NS stays - // current even when the manual bolus recommendation hasn't changed. - guard force || recommendationChanged else { - return - } - - lastManualBolusRecommendation = displayState.output?.recommendation?.manual?.loopKit - - if let output = displayState.output { - var dosingDecision = StoredDosingDecision(date: now, reason: "updateRemoteRecommendation") - dosingDecision.algorithmExperiments = algorithmExperiments(for: currentDosingStrategy, output: output) - dosingDecision.predictedGlucose = output.predictedGlucose.map(\.loopKit) - dosingDecision.insulinOnBoard = displayState.activeInsulin - dosingDecision.carbsOnBoard = displayState.activeCarbs - switch output.recommendationResult { - case .success(let recommendation): - dosingDecision.automaticDoseRecommendation = recommendation.automatic?.loopKit - if let recommendationDate = displayState.input?.predictionStart, let manualRec = recommendation.manual { - dosingDecision.manualBolusRecommendation = ManualBolusRecommendationWithDate(recommendation: manualRec.loopKit, date: recommendationDate) - } - case .failure(let error): - if let loopError = error as? LoopError { - dosingDecision.errors.append(loopError.issue) - } else { - dosingDecision.errors.append(.init(id: "error", details: ["description": error.localizedDescription])) - } - } - - dosingDecision.controllerStatus = UIDevice.current.controllerStatus - - // Device status for the Nightscout devicestatus.pump payload. - dosingDecision.pumpManagerStatus = deliveryDelegate?.pumpManagerStatus - if let pumpStatusHighlight = deliveryDelegate?.pumpStatusHighlight { - dosingDecision.pumpStatusHighlight = StoredDosingDecision.StoredDeviceHighlight( - localizedMessage: pumpStatusHighlight.localizedMessage, - imageName: pumpStatusHighlight.imageName, - state: pumpStatusHighlight.state) - } - dosingDecision.cgmManagerStatus = deliveryDelegate?.cgmManagerStatus - dosingDecision.lastReservoirValue = StoredDosingDecision.LastReservoirValue(doseStore.lastReservoirValue) - - self.logger.debug("Manual bolus rec = %{public}@", String(describing: dosingDecision.manualBolusRecommendation)) - await self.dosingDecisionStore.storeDosingDecision(dosingDecision) - } + await cycle.storeRemoteRecommendation(displayState: displayState, force: force) } - + // MARK: - Glucose Staleness private var glucoseValueStalenessTimer: Timer? @@ -1015,6 +527,28 @@ final class LoopDataManager: ObservableObject { } // MARK: - Background task management +extension LoopDataManager: LoopCycleHost { + var overrideHistory: TemporaryScheduleOverrideHistory { + temporaryPresetsManager.presetHistory + } + + var activeOverride: TemporaryScheduleOverride? { + temporaryPresetsManager.activeOverride + } + + var holdsPump: Bool { + true + } + + var isClosedLoop: Bool { + settingsProvider.dosingEnabled + } + + var controllerStatus: StoredDosingDecision.ControllerStatus { + UIDevice.current.controllerStatus + } +} + extension LoopDataManager: PersistenceControllerDelegate { nonisolated func persistenceControllerWillSave(_ controller: PersistenceController) { Task { @@ -1081,50 +615,17 @@ extension LoopDataManager { } } - private var currentDosingStrategy: AutomaticDosingStrategy { + var currentDosingStrategy: AutomaticDosingStrategy { dosingStrategySelectionEnabled ? settingsProvider.settings.automaticDosingStrategy : .automaticBolus } - /// Glucose based partial application only affects automatic boluses, so it is in effect only with that strategy - private func algorithmExperiments(for dosingStrategy: AutomaticDosingStrategy, output: AlgorithmOutput? = nil) -> [AlgorithmExperiment] { - enabledAlgorithmExperiments - .filter { $0 != .glucoseBasedApplicationFactor || dosingStrategy == .automaticBolus } - .sorted { $0.rawValue < $1.rawValue } - .map { experiment in - switch experiment { - case .guardAndRunway: - return guardAndRunwayExperiment(output?.guardAndRunway) - case .slowRetrospectiveCorrection: - var parameters: [String: JSONValue] = [:] - if let slow = output?.slowRetrospectiveCorrection { - parameters["level"] = .number(slow.level) - parameters["multiplier"] = .number(slow.multiplier) - } - return AlgorithmExperiment(identifier: experiment.rawValue, parameters: parameters.isEmpty ? nil : .object(parameters)) - default: - return AlgorithmExperiment(identifier: experiment.rawValue) - } - } - } - - private func guardAndRunwayExperiment(_ guardAndRunway: GuardAndRunwayOutput?) -> AlgorithmExperiment { - var parameters: [String: JSONValue] = ["version": .number(2)] - if let baseline = volatilityBaseline?.value { - parameters["volatilityBaseline"] = .number(baseline) - } - if let guardAndRunway { - parameters["volatility"] = .number(guardAndRunway.volatility) - parameters["bandApplied"] = .bool(guardAndRunway.bandApplied) - parameters["bandGain"] = .number(guardAndRunway.bandGain) - parameters["bandDepth"] = .number(guardAndRunway.bandDepth) - parameters["runwayShift"] = .number(guardAndRunway.runwayShift) - parameters["postLowRiseCutActive"] = .bool(guardAndRunway.postLowRiseCutActive) - } - return AlgorithmExperiment(identifier: ExperimentOption.guardAndRunway.rawValue, parameters: .object(parameters)) + func algorithmExperiments(for dosingStrategy: AutomaticDosingStrategy, output: AlgorithmOutput? = nil) -> [AlgorithmExperiment] { + AlgorithmExperiment.records(enabled: enabledAlgorithmExperiments, dosingStrategy: dosingStrategy, + volatilityBaseline: volatilityBaseline?.value, output: output) } /// The median σ5 over the last four weeks, for guard + runway's volatility band. Nil until there is a day of glucose. - private func currentVolatilityBaseline(at date: Date) async -> Double? { + func currentVolatilityBaseline(at date: Date) async -> Double? { if let volatilityBaseline, abs(date.timeIntervalSince(volatilityBaseline.date)) < (volatilityBaseline.value == nil ? .hours(1) : .days(1)) { return volatilityBaseline.value } @@ -1140,26 +641,7 @@ extension LoopDataManager { } func storeManualBolusDosingDecision(_ bolusDosingDecision: BolusDosingDecision, withDate date: Date) async { - var dosingDecision = StoredDosingDecision(id: bolusDosingDecision.id, - date: date, - reason: bolusDosingDecision.reason.rawValue, - settings: StoredDosingDecision.Settings(settingsProvider.settings), - scheduleOverride: bolusDosingDecision.scheduleOverride, - controllerStatus: UIDevice.current.controllerStatus, - pumpManagerStatus: deliveryDelegate?.pumpManagerStatus, - cgmManagerStatus: deliveryDelegate?.cgmManagerStatus, - lastReservoirValue: StoredDosingDecision.LastReservoirValue(doseStore.lastReservoirValue), - historicalGlucose: bolusDosingDecision.historicalGlucose, - originalCarbEntry: bolusDosingDecision.originalCarbEntry, - carbEntry: bolusDosingDecision.carbEntry, - manualGlucoseSample: bolusDosingDecision.manualGlucoseSample, - carbsOnBoard: bolusDosingDecision.carbsOnBoard, - insulinOnBoard: bolusDosingDecision.insulinOnBoard, - glucoseTargetRangeSchedule: bolusDosingDecision.glucoseTargetRangeSchedule, - predictedGlucose: bolusDosingDecision.predictedGlucose, - manualBolusRecommendation: bolusDosingDecision.manualBolusRecommendation, - manualBolusRequested: bolusDosingDecision.manualBolusRequested) - dosingDecision.algorithmExperiments = algorithmExperiments(for: currentDosingStrategy) + guard let dosingDecision = cycle.manualBolusDosingDecision(bolusDosingDecision, withDate: date) else { return } Task { await dosingDecisionStore.storeDosingDecision(dosingDecision) } } @@ -1287,128 +769,6 @@ extension LoopDataManager { } -extension NewCarbEntry { - var asStoredCarbEntry: StoredCarbEntry { - StoredCarbEntry( - startDate: startDate, - quantity: quantity, - foodType: foodType, - absorptionTime: absorptionTime, - userCreatedDate: date - ) - } -} - -extension CarbEntryUpdate { - var asStoredCarbEntry: StoredCarbEntry { - StoredCarbEntry( - startDate: startDate, - quantity: quantity, - foodType: foodType, - absorptionTime: absorptionTime, - userCreatedDate: date - ) - } -} - -extension NewGlucoseSample { - var asStoredGlucoseSample: StoredGlucoseSample { - StoredGlucoseSample( - syncIdentifier: syncIdentifier, - syncVersion: syncVersion, - startDate: date, - quantity: quantity, - condition: condition, - trend: trend, - trendRate: trendRate, - isDisplayOnly: isDisplayOnly, - wasUserEntered: wasUserEntered, - device: device - ) - } -} - - -extension StoredDataAlgorithmInput { - - func addingDose(dose: InsulinDoseType?) -> StoredDataAlgorithmInput { - var rval = self - if let dose { - rval.doses = doses + [dose] - } - return rval - } - - func addingGlucoseSample(sample: GlucoseType?) -> StoredDataAlgorithmInput { - var rval = self - if let sample { - rval.glucoseHistory.append(sample) - } - return rval - } - - func addingCarbEntry(carbEntry: CarbType?) -> StoredDataAlgorithmInput { - var rval = self - if let carbEntry { - rval.carbEntries = carbEntries + [carbEntry] - } - return rval - } - - func removingCarbEntry(carbEntry: CarbType?) -> StoredDataAlgorithmInput { - guard let carbEntry else { - return self - } - var rval = self - var currentEntries = self.carbEntries - if let index = currentEntries.firstIndex(of: carbEntry) { - currentEntries.remove(at: index) - } - rval.carbEntries = currentEntries - return rval - } - - func validateCarbScheduleCoverage() throws { - // CarbMath traps if either lookup is nil. Validate the final input, including pending - // additions/edits, so incomplete history becomes a recoverable recommendation error. - for entry in carbEntries { - guard carbRatio.closestPrior(to: entry.startDate) != nil else { - throw LoopError.configurationError(.carbRatioSchedule) - } - guard sensitivityMgdlPerUnit.closestPrior(to: entry.startDate) != nil else { - throw LoopError.configurationError(.insulinSensitivitySchedule) - } - } - } - - /// `sensitivityMultiplier` is slow retrospective correction's, from the algorithm's last run. - func predictGlucose(effectsOptions: AlgorithmEffectsOptions = .all, sensitivityMultiplier: Double = 1) throws -> [LoopAlgorithm.PredictedGlucoseValue] { - try validateCarbScheduleCoverage() - let prediction = Algorithm.generatePrediction( - start: predictionStart, - glucoseHistory: glucoseHistory, - doses: doses, - carbEntries: carbEntries, - basal: basal, - sensitivityMgdlPerUnit: sensitivityMgdlPerUnit.map { - LoopAlgorithm.AbsoluteScheduleValue(startDate: $0.startDate, endDate: $0.endDate, value: $0.value * sensitivityMultiplier) - }, - carbRatio: carbRatio, - algorithmEffectsOptions: effectsOptions, - useIntegralRetrospectiveCorrection: self.useIntegralRetrospectiveCorrection, - useMidAbsorptionISF: true, - carbAbsorptionModel: self.carbAbsorptionModel.model, - experiments: experiments - ) - return prediction.glucose - } -} - -extension Notification.Name { - static let LoopDataUpdated = Notification.Name(rawValue: "com.loopkit.Loop.LoopDataUpdated") - static let LoopRunning = Notification.Name(rawValue: "com.loopkit.Loop.LoopRunning") - static let LoopCycleCompleted = Notification.Name(rawValue: "com.loopkit.Loop.LoopCycleCompleted") -} protocol BolusDurationEstimator: AnyObject { func estimateBolusDuration(bolusUnits: Double) -> TimeInterval? @@ -1423,15 +783,6 @@ private extension TemporaryScheduleOverride { } } -private extension StoredDosingDecision.LastReservoirValue { - init?(_ reservoirValue: ReservoirValue?) { - guard let reservoirValue = reservoirValue else { - return nil - } - self.init(startDate: reservoirValue.startDate, unitVolume: reservoirValue.unitVolume) - } -} - extension ManualBolusRecommendationWithDate { init?(_ bolusRecommendationDate: (recommendation: ManualBolusRecommendation, date: Date)?) { guard let bolusRecommendationDate = bolusRecommendationDate else { @@ -1441,15 +792,6 @@ extension ManualBolusRecommendationWithDate { } } -private extension StoredDosingDecision.Settings { - init?(_ settings: StoredSettings?) { - guard let settings = settings else { - return nil - } - self.init(syncIdentifier: settings.syncIdentifier) - } -} - extension LoopDataManager: ServicesManagerDelegate { // Remote Overrides @@ -1776,41 +1118,6 @@ extension LoopDataManager: ManualDoseViewModelDelegate { } -extension AutomaticDosingStrategy { - var recommendationType: DoseRecommendationType { - switch self { - case .tempBasalOnly: - return .tempBasal - case .automaticBolus: - return .automaticBolus - } - } -} - -extension StoredDosingDecision { - 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): - self.automaticDoseRecommendation = recommendation.automatic?.loopKit - case .failure(let error): - self.appendError(error as? LoopError ?? .unknownError(error)) - } - if let activeInsulin = output.activeInsulin { - self.insulinOnBoard = InsulinValue(startDate: input.predictionStart, value: activeInsulin) - } - if let activeCarbs = output.activeCarbs { - self.carbsOnBoard = CarbValue(startDate: input.predictionStart, value: activeCarbs) - } - self.predictedGlucose = output.predictedGlucose.map(\.loopKit) - } -} - -enum CancelActiveTempBasalReason: String { - case automaticDosingDisabled - case unreliableCGMData - case maximumBasalRateChanged -} extension LoopDataManager : AlgorithmDisplayStateProvider { var algorithmState: AlgorithmDisplayState { diff --git a/Loop/Managers/RemoteDataServicesManager.swift b/Loop/Managers/RemoteDataServicesManager.swift index 43c08ad936..882e02d54c 100644 --- a/Loop/Managers/RemoteDataServicesManager.swift +++ b/Loop/Managers/RemoteDataServicesManager.swift @@ -9,6 +9,7 @@ import os.log import Foundation import LoopKit +import LoopCore import UIKit enum RemoteDataType: String, CaseIterable { diff --git a/Loop/Managers/SettingsManager.swift b/Loop/Managers/SettingsManager.swift index 66c7aa2c9f..caab4dff6b 100644 --- a/Loop/Managers/SettingsManager.swift +++ b/Loop/Managers/SettingsManager.swift @@ -399,19 +399,6 @@ extension SettingsManager { } } -@MainActor -protocol SettingsProvider: Observable { - var settings: StoredSettings { get } - var dosingEnabled: Bool { get } - - func getBasalHistory(startDate: Date, endDate: Date) async throws -> [AbsoluteScheduleValue] - func getCarbRatioHistory(startDate: Date, endDate: Date) async throws -> [AbsoluteScheduleValue] - func getInsulinSensitivityHistory(startDate: Date, endDate: Date) async throws -> [AbsoluteScheduleValue] - func getTargetRangeHistory(startDate: Date, endDate: Date) async throws -> [AbsoluteScheduleValue>] - func getDosingLimits(at date: Date) async throws -> DosingLimits - func executeSettingsQuery(fromQueryAnchor queryAnchor: SettingsStore.QueryAnchor?, limit: Int, completion: @escaping (SettingsStore.SettingsQueryResult) -> Void) -} - extension SettingsManager: SettingsProvider { var settings: StoredSettings { storedSettings } diff --git a/Loop/Managers/WatchDataManager.swift b/Loop/Managers/WatchDataManager.swift index fd9c270140..04fa25da94 100644 --- a/Loop/Managers/WatchDataManager.swift +++ b/Loop/Managers/WatchDataManager.swift @@ -62,9 +62,9 @@ final class WatchDataManager: NSObject { watchSession?.activate() } - private let log = DiagnosticLog(category: "WatchDataManager") + let log = DiagnosticLog(category: "WatchDataManager") - private var watchSession: WCSession? = { + var watchSession: WCSession? = { if WCSession.isSupported() { return WCSession.default } else { @@ -273,13 +273,12 @@ final class WatchDataManager: NSObject { @MainActor private func createWatchContext(recommendingBolusFor potentialCarbEntry: NewCarbEntry? = nil, reason: String = "update") async -> WatchContext { - var dosingDecision = BolusDosingDecision(for: .watchBolus) - let glucose = loopDataManager.latestGlucose let reservoir = loopDataManager.lastReservoirValue let basalDeliveryState = deviceManager.pumpManager?.status.basalDeliveryState - let (_, algoOutput) = loopDataManager.displayState.asTuple + let displayState = loopDataManager.displayState + let (_, algoOutput) = displayState.asTuple let carbsOnBoard = loopDataManager.activeCarbs @@ -293,8 +292,6 @@ final class WatchDataManager: NSObject { context.glucoseTrendRate = glucoseDisplay.trendRate } - dosingDecision.carbsOnBoard = carbsOnBoard - context.cgmManagerState = self.deviceManager.cgmManager?.rawValue if let sharing = self.deviceManager.cgmManager as? DeviceConfigurationSharing, sharing.isConfigurationSharingEnabled { context.cgmConfiguration = sharing.exportConfiguration().rawValue @@ -308,14 +305,12 @@ final class WatchDataManager: NSObject { context.potentialCarbEntry = potentialCarbEntry - if let recommendedBolus = try? await loopDataManager.recommendManualBolus( + let recommendedBolus = try? await loopDataManager.recommendManualBolus( manualGlucoseSample: nil, carbEntryChange: potentialCarbEntry.map { .add($0) } - ) { + ) + if let recommendedBolus { context.recommendedBolusDose = recommendedBolus.amount - dosingDecision.manualBolusRecommendation = ManualBolusRecommendationWithDate( - recommendation: recommendedBolus, - date: Date()) log.debug("watch bolus recommended: %{public}@ (with carb entry: %{public}@", String(describing: recommendedBolus.amount), String(describing: potentialCarbEntry)) // TEMP DIAGNOSTIC (watch bolus doubling investigation) — remove before merge. @@ -329,17 +324,13 @@ final class WatchDataManager: NSObject { deviceManager.deviceLog.log(managerIdentifier: "WatchBolusDiag", deviceIdentifier: nil, type: .connection, message: diag, completion: nil) } - var historicalGlucose: [HistoricalGlucoseValue]? - if let glucose = glucose { var sample: StoredGlucoseSample? let historicalGlucoseStartDate = Date(timeIntervalSinceNow: -LoopCoreConstants.dosingDecisionHistoricalGlucoseInterval) if let input = loopDataManager.displayState.input { let start = min(historicalGlucoseStartDate, glucose.startDate) - let samples = input.glucoseHistory.filterDateRange(start, nil) - sample = samples.last - historicalGlucose = samples.filter { $0.startDate >= historicalGlucoseStartDate }.map { HistoricalGlucoseValue(startDate: $0.startDate, quantity: $0.quantity) } + sample = input.glucoseHistory.filterDateRange(start, nil).last } context.glucose = sample?.quantity context.glucoseDate = sample?.startDate @@ -371,9 +362,6 @@ final class WatchDataManager: NSObject { context.lastManualBolus = loopDataManager.lastManualBolus - dosingDecision.historicalGlucose = historicalGlucose - dosingDecision.insulinOnBoard = loopDataManager.activeInsulin - if let basalDeliveryState = basalDeliveryState, let basalSchedule = self.temporaryPresetsManager.basalRateScheduleApplyingOverrideHistory, let netBasal = basalDeliveryState.getNetBasal(basalSchedule: basalSchedule, maximumBasalRatePerHour: self.settingsManager.settings.maximumBasalRatePerHour) @@ -389,8 +377,6 @@ final class WatchDataManager: NSObject { } } - dosingDecision.predictedGlucose = algoOutput?.predictedGlucose.map(\.loopKit) - var preMealOverride = self.temporaryPresetsManager.preMealOverride if preMealOverride?.hasFinished() == true { preMealOverride = nil @@ -401,13 +387,12 @@ final class WatchDataManager: NSObject { scheduleOverride = nil } - dosingDecision.scheduleOverride = scheduleOverride - - if scheduleOverride != nil || preMealOverride != nil { - dosingDecision.glucoseTargetRangeSchedule = self.temporaryPresetsManager.effectiveCorrectionRangeSchedule(presumingMealEntry: potentialCarbEntry != nil) - } else { - dosingDecision.glucoseTargetRangeSchedule = settings.glucoseTargetRangeSchedule - } + let glucoseTargetRangeSchedule = scheduleOverride != nil || preMealOverride != nil + ? self.temporaryPresetsManager.effectiveCorrectionRangeSchedule(presumingMealEntry: potentialCarbEntry != nil) + : settings.glucoseTargetRangeSchedule + let dosingDecision = BolusDosingDecision.watchBolus(displayState: displayState, recommendation: recommendedBolus, + scheduleOverride: scheduleOverride, + glucoseTargetRangeSchedule: glucoseTargetRangeSchedule) // Remove any expired context dosing decisions and add new self.contextDosingDecisions = self.contextDosingDecisions.filter { (date, _) in date.timeIntervalSinceNow > self.contextDosingDecisionExpirationDuration } diff --git a/Loop/Models/BolusDosingDecision.swift b/Loop/Models/BolusDosingDecision.swift deleted file mode 100644 index c45317720b..0000000000 --- a/Loop/Models/BolusDosingDecision.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// BolusDosingDecision.swift -// Loop -// -// Created by Darin Krauss on 10/1/20. -// Copyright © 2020 LoopKit Authors. All rights reserved. -// - -import LoopKit - -struct BolusDosingDecision { - enum Reason: String { - case normalBolus - case simpleBolus - case watchBolus - } - - var id: UUID - var reason: Reason - var scheduleOverride: TemporaryScheduleOverride? - var historicalGlucose: [HistoricalGlucoseValue]? - var originalCarbEntry: StoredCarbEntry? - var carbEntry: StoredCarbEntry? - var manualGlucoseSample: StoredGlucoseSample? - var carbsOnBoard: CarbValue? - var insulinOnBoard: InsulinValue? - var glucoseTargetRangeSchedule: GlucoseRangeSchedule? - var predictedGlucose: [PredictedGlucoseValue]? - var manualBolusRecommendation: ManualBolusRecommendationWithDate? - var manualBolusRequested: Double? - - init(for reason: Reason) { - self.id = UUID() - self.reason = reason - } -} diff --git a/Loop/Models/InsulinDeliveryHistory.swift b/Loop/Models/InsulinDeliveryHistory.swift index 55febcdbf1..222aa04f56 100644 --- a/Loop/Models/InsulinDeliveryHistory.swift +++ b/Loop/Models/InsulinDeliveryHistory.swift @@ -7,6 +7,8 @@ import Foundation import LoopKit +import LoopCore +import LoopAlgorithm struct InsulinDeliveryTotals: Equatable { var basal: Double = 0 diff --git a/Loop/Models/LoopConstants.swift b/Loop/Models/LoopConstants.swift deleted file mode 100644 index a7b484475f..0000000000 --- a/Loop/Models/LoopConstants.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// LoopConstants.swift -// Loop -// -// Created by Pete Schwamb on 10/7/20. -// Copyright © 2020 LoopKit Authors. All rights reserved. -// - -import Foundation -import LoopKit -import UIKit - -enum LoopConstants { - - // Input field bounds - - static let maxCarbEntryQuantity = LoopQuantity(unit: .gram, doubleValue: 250) // cannot exceed this value - - static let warningCarbEntryQuantity = LoopQuantity(unit: .gram, doubleValue: 99) // user is warned above this value - - static let validManualGlucoseEntryRange = LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 10)...LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 600) - - static let minCarbAbsorptionTime = TimeInterval(minutes: 30) - static let maxCarbAbsorptionTime = TimeInterval(hours: 8) - - static let maxCarbEntryPastTime = TimeInterval(hours: (-12)) - static let maxCarbEntryFutureTime = TimeInterval(hours: 1) - - static let maxOverrideDurationTime = TimeInterval(hours: 24) - - // MARK - Display settings - - static let minimumChartWidthPerHour: CGFloat = 50 - - static let statusChartMinimumHistoryDisplay: TimeInterval = .hours(1) - - /// Chart width the time window options are sized for: a portrait iPhone. - static let statusChartReferenceWidth: CGFloat = 360 - - static let statusChartMaximumWidthScale: CGFloat = 2 - - static let chartMinimumWidthPerTimeLabel: CGFloat = 44 - - static let glucoseChartDefaultDisplayBound = - LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 100)...LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 175) - - static let glucoseChartDefaultDisplayRangeWide = - LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 60)...LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 200) - - static let glucoseChartDefaultDisplayBoundClamped = - LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 80)...LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 240) - - - // Compile time configuration - - static let retrospectiveCorrectionEnabled = true - - /// Loop completion aging category limits - static let completionFreshLimit = TimeInterval(minutes: 6) - static let completionAgingLimit = TimeInterval(minutes: 16) - static let completionStaleLimit = TimeInterval(hours: 12) - - static let batteryReplacementDetectionThreshold = 0.5 - - static let defaultWatchCarbPickerValue = 15 // grams - - static let defaultWatchBolusPickerValue = 1.0 // % - - /// Missed Meal warning constants - static let missedMealWarningGlucoseRiseThreshold = 3.0 // mg/dL/m - static let missedMealWarningGlucoseRecencyWindow = TimeInterval(minutes: 19) - - // Bolus calculator warning thresholds - static let simpleBolusCalculatorMinGlucoseBolusRecommendation = LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 70) - static let simpleBolusCalculatorMinGlucoseMealBolusRecommendation = LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 55) - static let simpleBolusCalculatorGlucoseWarningLimit = LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 70) -} diff --git a/Loop/Models/StoredDataAlgorithmInput.swift b/Loop/Models/StoredDataAlgorithmInput.swift deleted file mode 100644 index 1708a98a55..0000000000 --- a/Loop/Models/StoredDataAlgorithmInput.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// StoredDataAlgorithmInput.swift -// Loop -// -// Created by Pete Schwamb on 2/23/24. -// Copyright © 2024 LoopKit Authors. All rights reserved. -// - -import Foundation -import LoopKit -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: [LoopAlgorithm.AbsoluteScheduleValue] - - var sensitivityMgdlPerUnit: [LoopAlgorithm.AbsoluteScheduleValue] - - var carbRatio: [LoopAlgorithm.AbsoluteScheduleValue] - - var targetMgdl: LoopAlgorithm.GlucoseRangeTimeline - - var suspendThresholdMgdl: Double? - - var maxBolus: Double - - var maxBasalRate: Double - - var useIntegralRetrospectiveCorrection: Bool - - var includePositiveVelocityAndRC: Bool - - var carbAbsorptionModel: CarbAbsorptionModel - - var recommendationInsulinModel: LoopAlgorithm.InsulinModel - - var recommendationType: DoseRecommendationType - - var automaticBolusApplicationFactor: Double? - - let useMidAbsorptionISF: Bool = true - - var maxActiveInsulinMultiplier: Double? = nil - - var gradualTransitionsThreshold: Double? = nil - - var experiments: Set = [] - - var volatilityBaseline: Double? = nil -} diff --git a/Loop/View Models/CurrentSetupViewModel.swift b/Loop/View Models/CurrentSetupViewModel.swift index 2d3d45b8b2..5f16a185cb 100644 --- a/Loop/View Models/CurrentSetupViewModel.swift +++ b/Loop/View Models/CurrentSetupViewModel.swift @@ -7,6 +7,7 @@ import Foundation import LoopKit +import LoopCore @MainActor final class CurrentSetupViewModel: ObservableObject { diff --git a/Loop/View Models/FavoriteFoodAddEditViewModel.swift b/Loop/View Models/FavoriteFoodAddEditViewModel.swift index 1140db0df0..360405e52d 100644 --- a/Loop/View Models/FavoriteFoodAddEditViewModel.swift +++ b/Loop/View Models/FavoriteFoodAddEditViewModel.swift @@ -8,6 +8,8 @@ import SwiftUI import LoopKit +import LoopCore +import LoopAlgorithm final class FavoriteFoodAddEditViewModel: ObservableObject { enum Alert: Identifiable { diff --git a/Loop/View Models/FavoriteFoodInsightsViewModel.swift b/Loop/View Models/FavoriteFoodInsightsViewModel.swift index 04e87d13be..244a92d80a 100644 --- a/Loop/View Models/FavoriteFoodInsightsViewModel.swift +++ b/Loop/View Models/FavoriteFoodInsightsViewModel.swift @@ -7,6 +7,8 @@ // import LoopKit +import LoopCore +import LoopKitUI import LoopAlgorithm import LoopKitUI import os.log diff --git a/Loop/View Models/StatisticsViewModel.swift b/Loop/View Models/StatisticsViewModel.swift index 5b386adf6c..d2078f2017 100644 --- a/Loop/View Models/StatisticsViewModel.swift +++ b/Loop/View Models/StatisticsViewModel.swift @@ -7,6 +7,8 @@ import Foundation import LoopKit +import LoopCore +import LoopAlgorithm @MainActor final class StatisticsViewModel: ObservableObject { diff --git a/Loop/Views/BolusEntryView.swift b/Loop/Views/BolusEntryView.swift index 6023a11e1c..0f976a59e3 100644 --- a/Loop/Views/BolusEntryView.swift +++ b/Loop/Views/BolusEntryView.swift @@ -9,6 +9,7 @@ import Combine import SwiftUI import LoopKit +import LoopCore import LoopKitUI import LoopUI diff --git a/Loop/Views/Insulin Delivery Log/InsulinDeliveryLogViewModel.swift b/Loop/Views/Insulin Delivery Log/InsulinDeliveryLogViewModel.swift index ca089e7b95..2e3eff6ac1 100644 --- a/Loop/Views/Insulin Delivery Log/InsulinDeliveryLogViewModel.swift +++ b/Loop/Views/Insulin Delivery Log/InsulinDeliveryLogViewModel.swift @@ -7,6 +7,7 @@ // import LoopKit +import LoopCore import os.log @MainActor diff --git a/Loop/Views/Presets/PresetsHistoryView.swift b/Loop/Views/Presets/PresetsHistoryView.swift index 519fa620c4..419cd6281a 100644 --- a/Loop/Views/Presets/PresetsHistoryView.swift +++ b/Loop/Views/Presets/PresetsHistoryView.swift @@ -7,6 +7,7 @@ // import LoopKit +import LoopCore import LoopKitUI import SwiftUI diff --git a/Loop/Views/Presets/PresetsPerformanceHistoryViewModel.swift b/Loop/Views/Presets/PresetsPerformanceHistoryViewModel.swift index 6a00cfffd0..5191ce1ec4 100644 --- a/Loop/Views/Presets/PresetsPerformanceHistoryViewModel.swift +++ b/Loop/Views/Presets/PresetsPerformanceHistoryViewModel.swift @@ -7,6 +7,7 @@ // import LoopKit +import LoopCore @MainActor @Observable diff --git a/Loop/Views/Statistics/StatisticsView.swift b/Loop/Views/Statistics/StatisticsView.swift index 1befaa5062..752ba229eb 100644 --- a/Loop/Views/Statistics/StatisticsView.swift +++ b/Loop/Views/Statistics/StatisticsView.swift @@ -7,6 +7,7 @@ import SwiftUI import LoopKit +import LoopCore import LoopKitUI import LoopUI diff --git a/Loop/Extensions/DosingDecisionStore.swift b/LoopCore/Extensions/DosingDecisionStore.swift similarity index 80% rename from Loop/Extensions/DosingDecisionStore.swift rename to LoopCore/Extensions/DosingDecisionStore.swift index 74d113dada..515f934db0 100644 --- a/Loop/Extensions/DosingDecisionStore.swift +++ b/LoopCore/Extensions/DosingDecisionStore.swift @@ -9,7 +9,7 @@ import Foundation import LoopKit -extension StoredDosingDecision { +public extension StoredDosingDecision { mutating func appendWarning(_ warning: LoopWarning) { warnings.append(warning.issue) } mutating func appendWarnings(_ warnings: [LoopWarning]) { warnings.forEach{ appendWarning($0) } } @@ -17,8 +17,8 @@ extension StoredDosingDecision { mutating func appendErrors(_ errors: [LoopError]) { errors.forEach{ appendError($0) } } } -enum StoredDosingDecisionIssue { - static func description(for error: Error?) -> String? { +public enum StoredDosingDecisionIssue { + public static func description(for error: Error?) -> String? { guard let error = error else { return nil } @@ -29,14 +29,14 @@ enum StoredDosingDecisionIssue { } } - static func description(for date: Date?) -> String? { + public static func description(for date: Date?) -> String? { guard let date = date else { return nil } return Self.dateFormatter.string(from: date) } - static var dateFormatter: ISO8601DateFormatter = { + public static var dateFormatter: ISO8601DateFormatter = { let dateFormatter = ISO8601DateFormatter() dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] return dateFormatter diff --git a/Loop/Extensions/PumpManagerError.swift b/LoopCore/Extensions/PumpManagerError.swift similarity index 96% rename from Loop/Extensions/PumpManagerError.swift rename to LoopCore/Extensions/PumpManagerError.swift index 79c6c4e87a..02662e18f7 100644 --- a/Loop/Extensions/PumpManagerError.swift +++ b/LoopCore/Extensions/PumpManagerError.swift @@ -8,7 +8,7 @@ import LoopKit -extension PumpManagerError { +public extension PumpManagerError { var issueId: String { switch self { case .configuration: diff --git a/Loop/Extensions/TempBasalRecommendation.swift b/LoopCore/Extensions/TempBasalRecommendation.swift similarity index 98% rename from Loop/Extensions/TempBasalRecommendation.swift rename to LoopCore/Extensions/TempBasalRecommendation.swift index f5cae224c3..094ae13b0b 100644 --- a/Loop/Extensions/TempBasalRecommendation.swift +++ b/LoopCore/Extensions/TempBasalRecommendation.swift @@ -33,7 +33,7 @@ extension TempBasalRecommendation { /// 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( + public func adjustForCurrentDelivery( at date: Date, neutralBasalRate: Double, currentTempBasal: DoseEntry?, diff --git a/LoopCore/Localizable.xcstrings b/LoopCore/Localizable.xcstrings index 85e409d185..8c22cc6369 100644 --- a/LoopCore/Localizable.xcstrings +++ b/LoopCore/Localizable.xcstrings @@ -293,6 +293,90 @@ "state" : "translated", "value" : "活性胰岛素" } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "أنسولين نشط" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktivt insulin" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulina activa" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Akt. insuliini" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "אינסולין פעיל" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulina attiva" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "残存インスリン" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktywna insulina" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulina Ativa" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Активный инсулин" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktívny inzulín" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktivt insulin" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktif İnsülin" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lượng Insulin còn hoạt động" + } } } }, @@ -508,667 +592,3043 @@ } } }, - "COB" : { - "comment" : "Label used for the Carbohydrates On Board value in the Live Activity view", + "Basal Rate Schedule" : { + "comment" : "Details for configuration error when basal rate schedule is missing", "localizations" : { - "de" : { + "ar" : { "stringUnit" : { "state" : "translated", - "value" : "COB" + "value" : "جدول الضخ المستمر" } }, - "fr" : { + "da" : { "stringUnit" : { "state" : "translated", - "value" : "Glucides" + "value" : "Basalrateskema" } }, - "nb" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "COB" + "value" : "Basalraten-Plan" } }, - "nl" : { + "es" : { "stringUnit" : { "state" : "translated", - "value" : "COB" + "value" : "Perfil Basal" } }, - "ro" : { + "fi" : { "stringUnit" : { "state" : "translated", - "value" : "COB" + "value" : "Basaaliohjelma" } }, - "zh-Hans" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "活性碳水化合物 (COB)" + "value" : "Programme débit basal" } - } - } - }, - "Current BG" : { - "comment" : "Label not shown in the Live Activity view", - "localizations" : { - "de" : { + }, + "he" : { "stringUnit" : { "state" : "translated", - "value" : "Aktueller BZ" + "value" : "Basal Rate Schedule" } }, - "fr" : { + "it" : { "stringUnit" : { "state" : "translated", - "value" : "Glycémie actuelle" + "value" : "Tabella di velocità basale" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "基礎レートスケジュール" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Nåværende BG" + "value" : "Tidsplan for basaldoser" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Huidige BG" + "value" : "Basaalstandenschema" } }, - "ro" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Glicemie curentă" + "value" : "Harmonogram dawki standardowej" } }, - "zh-Hans" : { + "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "当前血糖" + "value" : "Programação da Taxa Basal" } - } - } - }, - "Current Glucose" : { - "comment" : "Description of a bottom row item that shows the current glucose level.", - "extractionState" : "stale", - "isCommentAutoGenerated" : true, - "localizations" : { - "de" : { + }, + "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Aktueller Blutzucker" + "value" : "Orar rate bazale" } }, - "fr" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Glycémie actuelle" + "value" : "Скорости базала" } }, - "nb" : { + "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Nåværende glukoseverdi" + "value" : "Harmonogram bazálnej dávky" } }, - "nl" : { + "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Huidige glucose" + "value" : "Basaldosschema" } }, - "ro" : { + "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Glicemia curentă" + "value" : "Bazal Oran Çizelgesi" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lịch biểu tiêm liều basal" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "当前血糖" + "value" : "基础率表" } } } }, - "Current Glucose (Value and Arrow)" : { - "comment" : "Description for the Current Glucose selection for the Live Activity configuration", + "Carb effects" : { + "comment" : "Details for missing data error when carb effects are missing", "localizations" : { - "de" : { + "ar" : { "stringUnit" : { "state" : "translated", - "value" : "aktueller BZ (Wert und Trend)" + "value" : "تأثيرات الكارب" } }, - "fr" : { + "da" : { "stringUnit" : { "state" : "translated", - "value" : "Glycémie actuelle (valeur et flèche)" + "value" : "Kulhydrateffekter" } }, - "nb" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "Nåværende Glukose (Verdi og Pil)" + "value" : "Kohlenhydrat-Wirkungen" } }, - "nl" : { + "es" : { "stringUnit" : { "state" : "translated", - "value" : "Huidige glucose (waarde en pijl)" + "value" : "Efectos de carbohidratos" } }, - "ro" : { + "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Glicemia curentă (Valoare și tendință)" + "value" : "Hiilihydraattivaikutus" } }, - "zh-Hans" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "当前血糖(数值和箭头)" + "value" : "Effets des glucides" } - } - } - }, - "Delta" : { - "comment" : "Label used for the Delta Glucose in the Live Activity view", - "localizations" : { - "de" : { + }, + "he" : { "stringUnit" : { "state" : "translated", - "value" : "Delta" + "value" : "השפעות פחמימות" } }, - "fr" : { + "it" : { "stringUnit" : { "state" : "translated", - "value" : "Delta" + "value" : "Effetto carboidrati" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "糖質効果" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Delta" + "value" : "Karb effekter" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Delta" + "value" : "Koolhydraateffecten" } }, - "ro" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Delta" + "value" : "Wpływ węglowodanów" } }, - "zh-Hans" : { + "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Delta (变化值)" + "value" : "Efeitos Carb" } - } - } - }, - "Delta Glucose (Delta)" : { - "comment" : "Description for the Delta Glucose selection for the Live Activity configuration", - "localizations" : { - "de" : { + }, + "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Delta-BZ (Delta)" + "value" : "Efecte carbohidrați" } }, - "fr" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Delta Glucose (Delta)" + "value" : "Влияние углеводов" } }, - "nb" : { + "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Delta Glukose (Delta)" + "value" : "Účinky sacharidov" } }, - "nl" : { + "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Glucoseverandering (Delta)" + "value" : "Kolhydrateffekter" } }, - "ro" : { + "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Delta glicemie (Delta)" + "value" : "Karbonhidrat etkileri" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "các tác động của Carb" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "血糖变化量(Delta)" + "value" : "碳水效果" } } } }, - "Eventual BG" : { - "comment" : "Label used for the Forecasted Glucose in the Live Activity view", + "Carb Ratio Schedule" : { + "comment" : "Details for configuration error when carb ratio schedule is missing", "localizations" : { - "de" : { + "da" : { "stringUnit" : { "state" : "translated", - "value" : "Progn. BZ" + "value" : "Tidsplan for kulhydratforhold" } }, - "fr" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "Glycémie éventuelle" + "value" : "Zeitplan für das Kohlenhydratverhältnis" } }, - "nb" : { + "es" : { "stringUnit" : { "state" : "translated", - "value" : "Eventuell BG" + "value" : "Calendario de ratio de carbohidratos" } }, - "nl" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Uiteindelijke BG" + "value" : "Programme des ratios Insuline-Glucides" } }, - "ro" : { + "it" : { "stringUnit" : { "state" : "translated", - "value" : "Glicemia eventuală" + "value" : "Tabella del rapporto carboidrati" } }, - "zh-Hans" : { + "nb" : { "stringUnit" : { "state" : "translated", - "value" : "最终血糖" + "value" : "Tidsplan for karbohydratforhold" } - } - } - }, - "Eventually" : { - "comment" : "Description of a glucose value that is expected to change in the future.", - "extractionState" : "stale", - "isCommentAutoGenerated" : true, - "localizations" : { - "de" : { + }, + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Zukünftig" + "value" : "Koolhydraatratioschema" } }, - "fr" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Éventuellement" + "value" : "Harmonogram Współczynnika Węglowodanowego" } }, - "nb" : { + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Programul raportului de carbohidrați" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Расписание соотношения углеводов" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Karb Oranı Programı" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "碳水系数配置表" + } + } + } + }, + "Check your CGM data source" : { + "comment" : "Recovery suggestion when glucose data is missing", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تأكد من مصدر قراءات السكر المستمرة" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kontroller din CGM-datakilde" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Überprüfe Deine CGM-Datenquelle." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verifique su fuente de datos CGM" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tarkista CGM-tietolähde" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vérifier votre source de données CGM" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "בדוק את מקור המידע שלך בחיישן" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Controlla la sorgente dati del sensore" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "CGM データソースを確認してください" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sjekk CGM" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Controleer je CGM gegevensbron" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sprawdź swój CGM" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verifique seu dispositivo CGM" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verificați sursa de date a senzorului de glicemie" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Проверьте источник мониторинга СК" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kontrollera din CGM:s datakälla" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "CGM veri kaynağınızı kontrol edin" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kiểm tra nguồn dữ liệu CGM của bạn" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "请检查你的动态血糖数据来源" + } + } + } + }, + "COB" : { + "comment" : "Label used for the Carbohydrates On Board value in the Live Activity view", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glucides" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "活性碳水化合物 (COB)" + } + } + } + }, + "Configuration Error: %1$@" : { + "comment" : "The error message displayed for configuration errors. (1: configuration error details)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "خطأ في المعطيات: %1$@" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Konfigurationsfejl: %1$@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Konfigurationsfehler: %1$@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error de Configuración: %1$@" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Määritysvirhe: %1$@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erreur de Configuration: %1$@" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuration Error: %1$@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Errore di configurazione: %1$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "設定エラー: %1$@" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Konfigurasjonsfeil: %1$@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuratiefout: %1$@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Błąd konfiguracji: %1$@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erro de Configuração: %1$@" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eroare configurare: %1$@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ошибка конфигурации: %1$@" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Konfigurationsfel %1$@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Konfigürasyon Hatası: %1$@" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lỗi cấu hình: %1$@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "配置错误:%1$@" + } + } + } + }, + "Current BG" : { + "comment" : "Label not shown in the Live Activity view", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktueller BZ" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glycémie actuelle" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nåværende BG" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Huidige BG" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glicemie curentă" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "当前血糖" + } + } + } + }, + "Current Glucose" : { + "comment" : "Description of a bottom row item that shows the current glucose level.", + "extractionState" : "stale", + "isCommentAutoGenerated" : true, + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktueller Blutzucker" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glycémie actuelle" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nåværende glukoseverdi" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Huidige glucose" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glicemia curentă" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "当前血糖" + } + } + } + }, + "Current Glucose (Value and Arrow)" : { + "comment" : "Description for the Current Glucose selection for the Live Activity configuration", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "aktueller BZ (Wert und Trend)" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glycémie actuelle (valeur et flèche)" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nåværende Glukose (Verdi og Pil)" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Huidige glucose (waarde en pijl)" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glicemia curentă (Valoare și tendință)" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "当前血糖(数值和箭头)" + } + } + } + }, + "Delta" : { + "comment" : "Label used for the Delta Glucose in the Live Activity view", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delta" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delta" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delta" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delta" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delta" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delta (变化值)" + } + } + } + }, + "Delta Glucose (Delta)" : { + "comment" : "Description for the Delta Glucose selection for the Live Activity configuration", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delta-BZ (Delta)" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delta Glucose (Delta)" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delta Glukose (Delta)" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glucoseverandering (Delta)" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delta glicemie (Delta)" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "血糖变化量(Delta)" + } + } + } + }, + "Eventual BG" : { + "comment" : "Label used for the Forecasted Glucose in the Live Activity view", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Progn. BZ" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glycémie éventuelle" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eventuell BG" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uiteindelijke BG" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glicemia eventuală" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "最终血糖" + } + } + } + }, + "Eventually" : { + "comment" : "Description of a glucose value that is expected to change in the future.", + "extractionState" : "stale", + "isCommentAutoGenerated" : true, + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zukünftig" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Éventuellement" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Omsider" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eventueel" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "În cele din urmă" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "最终" + } + } + } + }, + "Forecasted Glucose (Eventual BG)" : { + "comment" : "Description for the Forecasted Glucose selection for the Live Activity configuration", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Prognostizierter Glukosewert (Endgültiger Blutzuckerwert)" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glycémie prévue (glycémie finale)" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Forutsagt glukoseverdi (eventuell BG)" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voorspelde glucose" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glicemie prognozată (glicemie eventuală)" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "预测血糖(最终稳态血糖)" + } + } + } + }, + "Glucose data is %1$@ old" : { + "comment" : "The error message when glucose data is too old to be used. (1: glucose data age in minutes)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "قراءات السكر منذ %1$@ " + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Blodsukkerdata er %1$@ gamle" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Blutzuckerdaten sind %1$@ alt" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los datos de glucosa son de hace %1$@ " + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glukoositieto on %1$@ vanha" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Les dernières données de glucose remontent à %1$@" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glucose data is %1$@ old" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "I dati sulla glicemia sono vecchi di %1$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "血糖データは %1$@ 前のものです" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Blodsukkerdata er %1$@ gammel" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glucosegegevens zijn %1$@ oud" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dane o glukozie są nieaktualne od %1$@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Medida de glicose está %1$@ atrasada" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Datele despre glicemie au o vechime de %1$@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Данные о глюкозе устарели на %1$@" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glukosvärde är %1$@ gammalt" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ eski KŞ verisi" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dữ liệu đường huyết %1$@ cũ" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "葡萄糖%1$@分钟未更新" + } + } + } + }, + "Glucose data not available" : { + "comment" : "Description of error when glucose data is missing", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "قراءات السكر غير متوفرة" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Blodsukkerdata ikke tilgængelige" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Blutzuckerdaten nicht verfügbar" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los datos de glucosa no están disponibles" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glukoositietoja ei saatavilla" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Les données de glucose ne sont pas disponibles" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glucose data not available" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dati sulla glicemia non disponibili" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "グルコースデータがありません" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Blodsukkerdata er utilgjengelig" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glucosegegevens niet beschikbaar" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dane o glukozie są niedostępne" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Medida de glicose não disponível" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nu sunt disponibile date despre glicemie" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Данные гликемии недоступны" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glukosvärde saknas" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "KŞ verisi mevcut değil" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dữ liệu đường huyết không sẵn sàng" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "血糖数据不可用" + } + } + } + }, + "Glucose Target Range Schedule" : { + "comment" : "Details for configuration error when glucose target range schedule is missing", + "localizations" : { + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tidsplan for glukosemålområde" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zeitplan für den Blutzucker-Zielbereich" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Horario para el objetivo de glucosa" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Horaire de la plage cible de glycémie" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Programma di intervalli target di glicemia" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tidsplan for blodsukker målområde" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glucosestreefbereikschema" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Harmonogram zakresu docelowego glukozy" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Programul intervalului țintă de glicemie" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "График целевого диапазона глюкозы" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "KŞ Hedef Aralığı Programı" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "血糖目标范围表" + } + } + } + }, + "In which mode do you want to render the Live Activity" : { + "comment" : "Description of a setting that allows the user to choose between a large or small display of the Live Activity.", + "isCommentAutoGenerated" : true, + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "In welchem Modus soll die Live-Aktivität angezeigt werden" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dans quel mode voulez-vous afficher les Activités en direct" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "I hvilken modus vil du vise Live activity" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "In which mode do you want to render the Live Activity" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "În ce mod doriți să redați activitatea live" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "你希望使用哪种模式来渲染实时活动" + } + } + } + }, + "Insulin effects" : { + "comment" : "Details for missing data error when insulin effects are missing\nDetails for missing data error when insulin effects including pending insulin are missing", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تأثيرات الأنسولين" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulineffekter" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulinwirkungen" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Efectos de la insulina" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insuliinivaikutus" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Effets de l'insuline" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulin effects" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Effetto dell’insulina" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "インスリン効果" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulin effekt" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulineëffecten" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "wpływ insuliny" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Efeitos da Insulina" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Efecte insulină" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Влияние инсулина" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulineffekter" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İnsülin etkileri" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tác dụng của insulin" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "胰岛素效果" + } + } + } + }, + "Insulin Sensitivity Schedule" : { + "comment" : "Details for configuration error when insulin sensitivity schedule is missing", + "localizations" : { + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulinfølsomhed tidsplan" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulinempfindlichkeit Plan" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Horario de la sensibilidad a la insulina" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Horaire de facteur de sensibilité à l'insuline" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Programma di sensibilità insulinica" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulin følsomhet tidsplan" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulinegevoeligheidschema" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Harmonogram wrażliwości na insulinę" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Orarul Factorului de Sensibilitate la Insulină" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "График чувствительности к инсулину" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İnsülin Duyarlılık Programı" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "胰岛素敏感系数(ISF)方案" + } + } + } + }, + "Invalid glucose reading with a timestamp that is %1$@ in the future" : { + "comment" : "The error message when glucose data is in the future. (1: glucose data time in future in minutes)", + "localizations" : { + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ugyldig glukoseaflæsning med et tidsstempel, der er %1$@ i fremtiden" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ungültiger Blutzuckermesswert mit einem Zeitstempel, der %1$@ in der Zukunft liegt" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lectura de glucosa no válida de %1$@ en el futuro" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lecture de glucose non valide avec un horodatage situé à %1$@ dans le futur" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lettura della glicemia non valida con data e ora %1$@ nel futuro" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ugyldig blodsukkermåling med et tidsstempel som er %1$@ i fremtiden" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ongeldige glucosemeting met een tijdstempel dat %1$@ in de toekomst ligt." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nieprawidłowy odczyt glukozy ze znacznikiem czasu, który wynosi %1$@ w przyszłości" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Citirea invalidă a glicemiei cu un marcaj temporal care este %1$@ în viitor" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Неверное показание глюкозы с временной меткой, которая находится на %1$@ в будущем" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gelecekte %1$@ olan bir zaman damgasıyla geçersiz KŞ okuması" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "血糖读数无效,其时间戳为 %1$@ ,该时间点在未来" + } + } + } + }, + "IOB" : { + "comment" : "Label used for the Insulin On Board value in the Live Activity view", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insuline" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "IAB" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "活性胰岛素 (IOB)" + } + } + } + }, + "Large" : { + "comment" : "Name of the \"Large\" Live Activity mode.", + "extractionState" : "stale", + "isCommentAutoGenerated" : true, + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Groß" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grand" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stor" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Groot" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mare" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "展开模式" + } + } + } + }, + "Loop is already looping." : { + "comment" : "The error message displayed for LoopError.loopInProgress errors." + }, + "Manual temporary basal in progress. Automatic dosing is disabled." : { + "comment" : "The error message displayed for LoopError.manualTempBasalRunning errors." + }, + "Maximum Basal Rate Per Hour" : { + "comment" : "Details for configuration error when maximum basal rate per hour is missing", + "localizations" : { + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maksimal basalrate pr. time" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maximale Basalte pro Stunde" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tasa basal máxima por hora" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Débit Basal Maximum par heure" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "בזאלי מקסימלי לשעה" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Velocità basale massima all'ora" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maksimal basaldose pr. time" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maximale basaalstand per uur" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maksymalna dawka podstawowa na godzinę" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rata bazală maximă pe oră" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Максимальная базальная скорость в час" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Saat Başı Maksimum Bazal Oran" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "每小时最大基础速率" + } + } + } + }, + "Maximum Bolus" : { + "comment" : "Details for configuration error when maximum bolus is missing", + "localizations" : { + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maksimal bolus" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maximaler Bolus" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolo Máximo" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Suurin sallittu bolus" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus Maximum" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "בולוס מקסימלי" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolo massimo" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "最大ボーラス" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maks bolus" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maximale bolus" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maksymalny bolus" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus Máximo" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus maxim" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Максимальный Болюс" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maximal bolus" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maksimum Bolus" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Liều Bolus tối đa" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "最大剂量" + } + } + } + }, + "Missing data: %1$@" : { + "comment" : "The error message for missing data. (1: missing data details)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "بيانات مفقودة: %1$@" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Manglende data: %1$@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fehlende Daten: %1$@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Faltan Datos: %1$@" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tiedot puuttuvat: %1$@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Données manquantes: %1$@" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "נתונים חסרים: %1$@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dati mancanti: %1$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "データがありません: %1$@" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Manglende data: %1$@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ontbrekende gegevens: %1$@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Brakujące dane: %1$@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dados ausentes: %1$@" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Date lipsă: %1$@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пропущены данные: %1$@" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Saknar data: %1$@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eksik veri: %1$@" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dữ liệu thiếu: %1$@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "数据缺失: %1$@" + } + } + } + }, + "Momentum effects" : { + "comment" : "Details for missing data error when momentum effects are missing", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تأثيرات النشاط" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Momentumeffekter" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Momentum-Effekte" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Efectos de Momento" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Liikevaikutukset (momentum)" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Effets de momentum" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "השפעות מומנטום" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Effetti del momento" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "モメンタム効果" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Momentum effekter" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trendlijneffecten" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "wpływ pędu" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Efeitos de aceleração" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Efecte avânt" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Влияние динамики СК" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Momentumeffekter" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Momentum etkileri" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hiệu ứng động lượng" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "葡萄糖动量效应" + } + } + } + }, + "No connected devices, or failure during device connection" : { + "comment" : "The error message displayed for device connection errors.", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "لا يوجد أجهزة متصلة, أو يوجد خطأ أثناء الاتصال" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingen tilsluttede enheder eller fejl under forbindelse til enhed" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keine verbundenen Geräte oder Fehler während der Geräteverbindung" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No hay dispositivos conectados o falla durante conexión de dispositivo" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ei yhdistettyjä laitteita tai häiriö laiteyhteydessä" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pas d'appareil connecté, ou échec durant la connexion à l'appareil" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "אין מכשירים מחוברים, או שישנה שגיאת התחברות." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nessun dispositivo connesso o mancanza di segnale durante la connessione del dispositivo" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "機器が未接続、または接続に問題" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingen tilkoblede enheter, eller feil under enhetstilkobling" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geen aangesloten apparaten of storingen tijdens het verbinden met het apparaat" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Brak podłączonych urządzeń lub awaria podczas połączenia urządzenia" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nenhum dispositivo conectado ou falha durante a conexão" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nu este conectat niciun dispozitiv sau s-a produs o eroare la conectare" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Устройства не сопряжены или произошла ошибка во время сопряжения" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingen ansluten enhet, eller fel vid anslutning till enhet" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bağlı cihaz yok veya cihaz bağlantısı sırasında hata" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không có thiết bị nào được kết nối, hoặc lỗi trong quá trình kết nối" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "没有连接的设备,或设备连接期间发生故障" + } + } + } + }, + "Plot and Row" : { + "comment" : "Short name to choose the Lock Screen display including the the plot", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Diagramm und Text" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ligne et graphique" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Felt og rad" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grafiek en rij" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Parcelă și rând" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "图表和数据行" + } + } + } + }, + "Pump data is %1$@ old" : { + "comment" : "The error message when pump data is too old to be used. (1: pump data age in minutes)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "بيانات المضخة منذ %1$@ " + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pumpedata er %1$@ gammel" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pumpendaten sind %1$@ alt" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los datos de la microinfusora son %1$@ viejos" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pumpputieto on %1$@ vanha" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Les données de la pompe remontent à %1$@" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "מידע משאבה: לפני %1$@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "I dati forniti dalla pompa sono di %1$@ fa" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ポンプデータが %1$@前のものです" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pumpedata er %1$@ gammel" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pompgegevens zijn %1$@ oud" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dane z pompy są nieaktualne od %1$@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dados da bomba são de %1$@ atrás" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Datele din pompă sunt vechi de %1$@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Данные помпы от %1$@ " + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pumpvärdena är %1$@ gamla" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pompa verisi %1$@ eski" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dữ liệu bơm %1$@ là cũ" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "胰岛素泵数据%1$@分钟未更新" + } + } + } + }, + "Pump Inoperable. Automatic dosing is disabled." : { + "comment" : "The error message displayed for LoopError.pumpInoperable errors." + }, + "Pump Manager" : { + "comment" : "Details for configuration error when pump manager is missing", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "إدارة المضخة" + } + }, + "da" : { "stringUnit" : { "state" : "translated", - "value" : "Omsider" + "value" : "Pumpemanager" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pumpenmanager" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Administratión de Microinfusora" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pumpun hallinta" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gestionnaire de pompe" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "מנהל משאבה" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pump Manager" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ポンプ設定" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pumpe manager" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pomp manager" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zarządzanie Pompą" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gerenciamento da Bomba" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Manager pompă" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Менеджер помпо" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pumphantering" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pompa Yöneticisi" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trình quản lý bơm" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "泵管理" + } + } + } + }, + "Pump Manager Error: %1$@" : { + "comment" : "The error message displayed for pump manager errors. (1: pump manager error)", + "localizations" : { + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fejl i pumpemanager: %1$@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pumpen-Fehler: %1$@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error del administrador de bomba: %1$@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erreur du gestionnaire de pompe : %1$@" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "שגיאה במנהל שאבה: %1$@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Errore Pump Manager: %1$@" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pump Manager-feil: %1$@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pompmanager fout: %1$@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Błąd menedżera pompy: %1$@" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eroare de gestionare a pompei: %1$@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ошибка менеджера помп: %1$@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pompa Yöneticisi Hatası: %1$@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "泵管理器错误:%1$@" + } + } + } + }, + "Pump Suspended. Automatic dosing is disabled." : { + "comment" : "The error message displayed for LoopError.pumpSuspended errors.", + "localizations" : { + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pumpen er suspenderet. Automatisk dosering er deaktiveret." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pumpe unterbrochen. Die automatische Dosierung ist deaktiviert." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Microinfusora suspendida. La dosificación automática está desactivada." + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pumppu pysäytetty. Automaattinen annostelu ei ole käytössä." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pompe suspendue. Le dosage automatique est désactivé." + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "משאבה מושהית. מינון אוטומטי מושבת." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pompa sospesa. Il dosaggio automatico è disattivato." + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pumpe suspendert. Automatisk dosing er deaktivert." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pomp onderbroken. Automatisch doseren is uitgeschakeld." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pompa zawieszona. Automatyczne dozowanie jest wyłączone." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pompă suspendată. Administrarea automată este dezactivată." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Помпа приостановлена. Авто болюсы и ВБС отключены." + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pump pausad. Automatisk dosering är inaktiverad." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pompa Askıya Alındı. Otomatik dozlama devre dışı bırakıldı." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "泵已暂停。自动给药功能已关闭。" + } + } + } + }, + "Recommendation expired: %1$@ old" : { + "comment" : "The error message when a recommendation has expired. (1: age of recommendation in minutes)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "انتهت صلاحية التوصية منذ: %1$@ " + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Forslag udløbet: %1$@ gamle" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Empfehlung abgelaufen: %1$@ alt" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recomendación expiró hace: %1$@" } }, - "nl" : { + "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Eventueel" + "value" : "Suositus vanhentunut: %1$@ vanha" } }, - "ro" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "În cele din urmă" + "value" : "Recommandation expirée, remonte à %1$@" } }, - "zh-Hans" : { + "he" : { "stringUnit" : { "state" : "translated", - "value" : "最终" + "value" : "המלצה פגה: לפני %1$@" } - } - } - }, - "Forecasted Glucose (Eventual BG)" : { - "comment" : "Description for the Forecasted Glucose selection for the Live Activity configuration", - "localizations" : { - "de" : { + }, + "it" : { "stringUnit" : { "state" : "translated", - "value" : "Prognostizierter Glukosewert (Endgültiger Blutzuckerwert)" + "value" : "La raccomandazione è scaduta: %1$@ fa" } }, - "fr" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Glycémie prévue (glycémie finale)" + "value" : "推奨が %1$@ 経過したため失効" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Forutsagt glukoseverdi (eventuell BG)" + "value" : "Anbefaling utløpt: %1$@ gammelt" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Voorspelde glucose" + "value" : "Aanbeveling verlopen: %1$@ oud" } }, - "ro" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Glicemie prognozată (glicemie eventuală)" + "value" : "Rekomendacja nieaktualna od %1$@" } }, - "zh-Hans" : { + "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "预测血糖(最终稳态血糖)" + "value" : "Recomendação expirou: %1$@ atrás" } - } - } - }, - "In which mode do you want to render the Live Activity" : { - "comment" : "Description of a setting that allows the user to choose between a large or small display of the Live Activity.", - "isCommentAutoGenerated" : true, - "localizations" : { - "de" : { + }, + "ro" : { "stringUnit" : { "state" : "translated", - "value" : "In welchem Modus soll die Live-Aktivität angezeigt werden" + "value" : "Recomandare expirată: acum %1$@" } }, - "fr" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Dans quel mode voulez-vous afficher les Activités en direct" + "value" : "Рекомнендация истекла : от %1$@" } }, - "nb" : { + "sv" : { "stringUnit" : { "state" : "translated", - "value" : "I hvilken modus vil du vise Live activity" + "value" : "Rekommendationen gick ut för %1$@ sedan" } }, - "nl" : { + "tr" : { "stringUnit" : { "state" : "translated", - "value" : "In which mode do you want to render the Live Activity" + "value" : "Önerinin süresi doldu: %1$@ eski" } }, - "ro" : { + "vi" : { "stringUnit" : { "state" : "translated", - "value" : "În ce mod doriți să redați activitatea live" + "value" : "Khuyến cáo hết hạn: %1$@ phút" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "你希望使用哪种模式来渲染实时活动" + "value" : "%1$@分钟前推荐剂量已过期" } } } }, - "IOB" : { - "comment" : "Label used for the Insulin On Board value in the Live Activity view", + "Relative Basal Rate (Basal)" : { + "comment" : "Description for the Basal Rate plot selection for the Live Activity configuration", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", - "value" : "IOB" + "value" : "Relative Basalrate (Basal)" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Insuline" + "value" : "Débit basal relatif (Basal)" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "IOB" + "value" : "Netto basaldose (basal)" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "IAB" + "value" : "Relatieve basaalstand" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "IOB" + "value" : "Rată bazală relativă (bazală)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "活性胰岛素 (IOB)" + "value" : "相对基础率(基础率)" } } } }, - "Large" : { - "comment" : "Name of the \"Large\" Live Activity mode.", - "extractionState" : "stale", - "isCommentAutoGenerated" : true, + "Row Only" : { + "comment" : "Short name to choose the Lock Screen display without the plot", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", - "value" : "Groß" + "value" : "nur Text" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Grand" + "value" : "Ligne seule" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Stor" + "value" : "Kun rad" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Groot" + "value" : "Alleen rij" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Mare" + "value" : "Doar rând" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "展开模式" + "value" : "仅数据行" } } } }, - "Plot and Row" : { - "comment" : "Short name to choose the Lock Screen display including the the plot", + "Small" : { + "comment" : "Name of the \"Small\" Live Activity mode.", + "extractionState" : "stale", + "isCommentAutoGenerated" : true, "localizations" : { "de" : { "stringUnit" : { "state" : "translated", - "value" : "Diagramm und Text" + "value" : "Klein" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Ligne et graphique" + "value" : "Petit" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Felt og rad" + "value" : "Liten" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Grafiek en rij" + "value" : "Klein" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Parcelă și rând" + "value" : "Mic" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "图表和数据行" + "value" : "紧凑模式" } } } }, - "Relative Basal Rate (Basal)" : { - "comment" : "Description for the Basal Rate plot selection for the Live Activity configuration", + "Suspend Threshold" : { + "comment" : "The title text in settings", + "extractionState" : "manual", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "قيمة التعليق" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pause grænseværdi" + } + }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Relative Basalrate (Basal)" + "value" : "Grenzwert für Hypo-Abschaltung" } }, - "fr" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Débit basal relatif (Basal)" + "value" : "Suspend Threshold" } }, - "nb" : { + "es" : { "stringUnit" : { "state" : "translated", - "value" : "Netto basaldose (basal)" + "value" : "Nivel de Suspensión" } }, - "nl" : { + "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Relatieve basaalstand" + "value" : "Pysäytysraja" } }, - "ro" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Rată bazală relativă (bazală)" + "value" : "Seuil de suspension" } }, - "zh-Hans" : { + "he" : { "stringUnit" : { "state" : "translated", - "value" : "相对基础率(基础率)" + "value" : "סף השהייה" } - } - } - }, - "Row Only" : { - "comment" : "Short name to choose the Lock Screen display without the plot", - "localizations" : { - "de" : { + }, + "it" : { "stringUnit" : { "state" : "translated", - "value" : "nur Text" + "value" : "Soglia di sospensione" } }, - "fr" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Ligne seule" + "value" : "一時停止値" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Kun rad" + "value" : "Terskel for utsettelse" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Alleen rij" + "value" : "Onderbrekingsdrempel" } }, - "ro" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Doar rând" + "value" : "Próg zawieszenia pompy" } }, - "zh-Hans" : { + "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "仅数据行" + "value" : "Limite de Suspenção" } - } - } - }, - "Small" : { - "comment" : "Name of the \"Small\" Live Activity mode.", - "extractionState" : "stale", - "isCommentAutoGenerated" : true, - "localizations" : { - "de" : { + }, + "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Klein" + "value" : "Limită suspendare" } }, - "fr" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Petit" + "value" : "Порог приостановки" } }, - "nb" : { + "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Liten" + "value" : "Tröskelvärde" } }, - "nl" : { + "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Klein" + "value" : "Eşiği Askıya Al" } }, - "ro" : { + "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Mic" + "value" : "Ngưỡng Tạm dừng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "紧凑模式" + "value" : "暂停阈值" } } } @@ -1262,6 +3722,101 @@ } } }, + "Unknown Error: %1$@" : { + "comment" : "The error message displayed for unknown LoopError errors. (1: unknown error)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "خطأ غير معروف" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Neznámá chyba: %1$@" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ukendt fejl: %1$@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unbekannter Fehler: %1$@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error desconocido: %1$@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erreur inconnue : %1$@" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "תקלה לא ידועה %1$@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Errore sconosciuto: %1$@" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ukjent feil: %1$@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Onbekende fout: %1$@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nieznany błąd: %1$@" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eroare necunoscută: %1$@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Неизвестная ошибка: %1$@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bilinmeyen Hata: %1$@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未知错误:%1$@" + } + } + } + }, "Updated" : { "comment" : "Name of the \"Updated\" option in the Live Activity settings.", "extractionState" : "stale", diff --git a/LoopCore/Managers/LoopAlgorithmSupport.swift b/LoopCore/Managers/LoopAlgorithmSupport.swift new file mode 100644 index 0000000000..9dbbce389e --- /dev/null +++ b/LoopCore/Managers/LoopAlgorithmSupport.swift @@ -0,0 +1,540 @@ +// +// LoopAlgorithmSupport.swift +// Loop +// +// Code from LoopDataManager.swift that the watch app also runs, so both compile one definition. +// The declarations moved unchanged; `fetch` is `fetchData`'s body and `checkRecency` is `loop()`'s +// input checks, with LoopDataManager's own properties as parameters. LastReservoirValue and +// Settings were private there; the watch and LoopDataManager+PodLoan.swift use them. +// + +import Foundation +import LoopKit +import LoopAlgorithm + +public struct AlgorithmDisplayState { + public var input: StoredDataAlgorithmInput? + public var output: AlgorithmOutput? + + public init(input: StoredDataAlgorithmInput? = nil, output: AlgorithmOutput? = nil) { + self.input = input + self.output = output + } + + /// The ISF multiplier slow retrospective correction applied on the last run, 1 when it is off. + public var sensitivityMultiplier: Double { + output?.slowRetrospectiveCorrection?.multiplier ?? 1 + } + + public var activeInsulin: LoopKit.InsulinValue? { + guard let input, let value = output?.activeInsulin else { + return nil + } + return LoopKit.InsulinValue(startDate: input.predictionStart, value: value) + } + + public var activeCarbs: LoopKit.CarbValue? { + guard let input, let value = output?.activeCarbs else { + return nil + } + return LoopKit.CarbValue(startDate: input.predictionStart, value: value) + } + + public var asTuple: (algoInput: StoredDataAlgorithmInput?, algoOutput: AlgorithmOutput?) { + return (algoInput: input, algoOutput: output) + } +} + +public extension NewCarbEntry { + var asStoredCarbEntry: StoredCarbEntry { + StoredCarbEntry( + startDate: startDate, + quantity: quantity, + foodType: foodType, + absorptionTime: absorptionTime, + userCreatedDate: date + ) + } +} + +public extension NewGlucoseSample { + var asStoredGlucoseSample: StoredGlucoseSample { + StoredGlucoseSample( + syncIdentifier: syncIdentifier, + syncVersion: syncVersion, + startDate: date, + quantity: quantity, + condition: condition, + trend: trend, + trendRate: trendRate, + isDisplayOnly: isDisplayOnly, + wasUserEntered: wasUserEntered, + device: device + ) + } +} + +public extension CarbEntryUpdate { + var asStoredCarbEntry: StoredCarbEntry { + StoredCarbEntry( + startDate: startDate, + quantity: quantity, + foodType: foodType, + absorptionTime: absorptionTime, + userCreatedDate: date + ) + } +} + +public extension StoredDataAlgorithmInput { + + func addingDose(dose: InsulinDoseType?) -> StoredDataAlgorithmInput { + var rval = self + if let dose { + rval.doses = doses + [dose] + } + return rval + } + + func addingGlucoseSample(sample: GlucoseType?) -> StoredDataAlgorithmInput { + var rval = self + if let sample { + rval.glucoseHistory.append(sample) + } + return rval + } + + func addingCarbEntry(carbEntry: CarbType?) -> StoredDataAlgorithmInput { + var rval = self + if let carbEntry { + rval.carbEntries = carbEntries + [carbEntry] + } + return rval + } + + func removingCarbEntry(carbEntry: CarbType?) -> StoredDataAlgorithmInput { + guard let carbEntry else { + return self + } + var rval = self + var currentEntries = self.carbEntries + if let index = currentEntries.firstIndex(of: carbEntry) { + currentEntries.remove(at: index) + } + rval.carbEntries = currentEntries + return rval + } + + func validateCarbScheduleCoverage() throws { + // CarbMath traps if either lookup is nil. Validate the final input, including pending + // additions/edits, so incomplete history becomes a recoverable recommendation error. + for entry in carbEntries { + guard carbRatio.closestPrior(to: entry.startDate) != nil else { + throw LoopError.configurationError(.carbRatioSchedule) + } + guard sensitivityMgdlPerUnit.closestPrior(to: entry.startDate) != nil else { + throw LoopError.configurationError(.insulinSensitivitySchedule) + } + } + } + + /// `sensitivityMultiplier` is slow retrospective correction's, from the algorithm's last run. + func predictGlucose(effectsOptions: AlgorithmEffectsOptions = .all, sensitivityMultiplier: Double = 1) throws -> [LoopAlgorithm.PredictedGlucoseValue] { + try validateCarbScheduleCoverage() + let prediction = Algorithm.generatePrediction( + start: predictionStart, + glucoseHistory: glucoseHistory, + doses: doses, + carbEntries: carbEntries, + basal: basal, + sensitivityMgdlPerUnit: sensitivityMgdlPerUnit.map { + LoopAlgorithm.AbsoluteScheduleValue(startDate: $0.startDate, endDate: $0.endDate, value: $0.value * sensitivityMultiplier) + }, + carbRatio: carbRatio, + algorithmEffectsOptions: effectsOptions, + useIntegralRetrospectiveCorrection: self.useIntegralRetrospectiveCorrection, + useMidAbsorptionISF: true, + carbAbsorptionModel: self.carbAbsorptionModel.model, + experiments: experiments + ) + return prediction.glucose + } +} + +public extension Notification.Name { + static let LoopDataUpdated = Notification.Name(rawValue: "com.loopkit.Loop.LoopDataUpdated") + static let LoopRunning = Notification.Name(rawValue: "com.loopkit.Loop.LoopRunning") + static let LoopCycleCompleted = Notification.Name(rawValue: "com.loopkit.Loop.LoopCycleCompleted") +} + +public extension StoredDosingDecision.LastReservoirValue { + init?(_ reservoirValue: ReservoirValue?) { + guard let reservoirValue = reservoirValue else { + return nil + } + self.init(startDate: reservoirValue.startDate, unitVolume: reservoirValue.unitVolume) + } +} + +public extension StoredDosingDecision.Settings { + init?(_ settings: StoredSettings?) { + guard let settings = settings else { + return nil + } + self.init(syncIdentifier: settings.syncIdentifier) + } +} + +public extension AutomaticDosingStrategy { + var recommendationType: DoseRecommendationType { + switch self { + case .tempBasalOnly: + return .tempBasal + case .automaticBolus: + return .automaticBolus + } + } +} + +public extension StoredDosingDecision { + 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): + self.automaticDoseRecommendation = recommendation.automatic?.loopKit + case .failure(let error): + self.appendError(error as? LoopError ?? .unknownError(error)) + } + if let activeInsulin = output.activeInsulin { + self.insulinOnBoard = LoopKit.InsulinValue(startDate: input.predictionStart, value: activeInsulin) + } + if let activeCarbs = output.activeCarbs { + self.carbsOnBoard = LoopKit.CarbValue(startDate: input.predictionStart, value: activeCarbs) + } + self.predictedGlucose = output.predictedGlucose.map(\.loopKit) + } +} + +public extension StoredDataAlgorithmInput { + /// The body of `LoopDataManager.fetchData`, shared with the watch app. Each caller passes its own + /// sources; `isolation` keeps the work on the caller's actor (the main actor for LoopDataManager). + static func fetch( + for baseTime: Date?, + presumePresetEndingNow: Bool, + ensureDosingCoverageStart: Date?, + ensureCarbCoverageStart: Date? = nil, + projectOngoingDoses: Bool, + now: Date, + doseStore: DoseStoreProtocol, + carbStore: CarbStoreProtocol, + glucoseStore: GlucoseStoreProtocol, + settingsProvider: SettingsProvider, + overrideHistory: TemporaryScheduleOverrideHistory, + activeOverride: TemporaryScheduleOverride?, + pumpInsulinType: InsulinType?, + insulinModel insulinModelForType: (InsulinType?) -> LoopKit.InsulinModel, + experiments: Set, + volatilityBaseline: Double?, + carbAbsorptionModel: CarbAbsorptionModel, + isolation: isolated (any Actor)? = #isolation + ) async throws -> StoredDataAlgorithmInput { + func insulinModel(for type: InsulinType?) -> LoopKit.InsulinModel { insulinModelForType(type) } + + // Slow retrospective correction keeps no state, so it reads its whole window of glucose on every run, + // with the carbs and doses that explain it. + let slowRetrospectiveCorrectionHistory: TimeInterval? = experiments.contains(.slowRetrospectiveCorrection) ? .hours(72) : nil + + // Need to fetch doses back as far as t - (DIA + DCA) for Dynamic carbs + var dosesInputHistory = CarbMath.maximumAbsorptionTimeInterval + InsulinMath.defaultInsulinActivityDuration + if let slowRetrospectiveCorrectionHistory { + dosesInputHistory = max(dosesInputHistory, slowRetrospectiveCorrectionHistory + InsulinMath.defaultInsulinActivityDuration) + } + + let baseTime = baseTime ?? now + + var dosesStart = baseTime.addingTimeInterval(-dosesInputHistory) + + // Ensure dosing data goes back before ensureDosingCoverageStart, if specified + if let ensureDosingCoverageStart { + dosesStart = min(ensureDosingCoverageStart, dosesStart) + } + + // When projectOngoingDoses is true (display path), pass end:nil so DoseStore + // extends a mutable suspend to its insulin-activity-duration fallback. Doses + // already in flight (e.g. a manual temp basal) keep their actual endDate + // either way; only the suspend extension is gated on this flag. + let doses = try await doseStore.getNormalizedDoseEntries( + start: dosesStart, + end: projectOngoingDoses ? nil : baseTime + ) + + // Doses that were included because they cover dosesStart might have a start time earlier than dosesStart + // This moves the start time back to ensure basal covers + dosesStart = min(dosesStart, doses.map { $0.startDate }.min() ?? dosesStart) + + // Doses with a start time before baseTime might still end after baseTime + let dosesEnd = max(baseTime, doses.map { $0.endDate }.max() ?? baseTime) + + let rawBasal = try await settingsProvider.getBasalHistory(startDate: dosesStart, endDate: dosesEnd) + + guard !rawBasal.isEmpty else { + throw LoopError.configurationError(.basalRateSchedule) + } + + // Collapse contiguous same-rate basal entries. getBasalHistory projects the + // daily BasalRateSchedule onto absolute time and splits at every local + // midnight even when the rate doesn't change; InsulinDose.annotated(with:) + // then splits the suspend (or any long basal-typed dose) at each of those + // boundaries, and the continuous-delivery IOB integrator doesn't join the + // resulting sub-doses perfectly across the boundary -- visible as a small + // bump in Active Insulin at midnight even with a single-rate schedule. + let basal: [LoopKit.AbsoluteScheduleValue] = rawBasal.reduce(into: []) { acc, entry in + if let last = acc.last, last.value == entry.value, last.endDate == entry.startDate { + acc[acc.count - 1] = LoopKit.AbsoluteScheduleValue(startDate: last.startDate, endDate: entry.endDate, value: last.value) + } else { + acc.append(entry) + } + } + + let forecastEndTime = baseTime.addingTimeInterval(InsulinMath.defaultInsulinActivityDuration).dateCeiledToTimeInterval(GlucoseMath.defaultDelta) + + var glucoseStart = baseTime.addingTimeInterval(LoopConstants.maxCarbEntryPastTime + .minutes(-1)) // additional minute to handle difference in seconds between carb entry and carb ratio + var carbsStart = glucoseStart + if let slowRetrospectiveCorrectionHistory { + glucoseStart = baseTime.addingTimeInterval(-slowRetrospectiveCorrectionHistory) + carbsStart = glucoseStart.addingTimeInterval(-CarbMath.maximumAbsorptionTimeInterval + .minutes(-1)) + } + // A pending carb entry can outlive this rolling window while the bolus screen is open. + // Cover it with schedule history without expanding the stored-carb or glucose queries. + let carbScheduleStart = min(carbsStart, ensureCarbCoverageStart ?? carbsStart) + + // Include future carbs in query, but filter out ones entered after basetime. The filtering is only applicable when running in a retrospective situation. + let carbEntries = try await carbStore.getCarbEntries( + start: carbsStart, + end: forecastEndTime + ).filter { + $0.userCreatedDate ?? $0.startDate < baseTime + } + + let carbRatio = try await settingsProvider.getCarbRatioHistory( + startDate: carbScheduleStart, + endDate: forecastEndTime + ) + + guard !carbRatio.isEmpty else { + throw LoopError.configurationError(.carbRatioSchedule) + } + + let glucose = try await glucoseStore.getGlucoseSamples(start: glucoseStart, end: baseTime) + + let dosesWithModel = doses.map { $0.simpleDose(with: insulinModel(for: $0.insulinType)) } + + let recommendationInsulinModel = insulinModel(for: pumpInsulinType ?? .novolog) + + let recommendationEffectInterval = DateInterval( + start: baseTime, + duration: recommendationInsulinModel.effectDuration + ) + let neededSensitivityTimeline = Algorithm.timelineIntervalForSensitivity( + doses: dosesWithModel, + glucoseHistoryStart: glucose.first?.startDate ?? baseTime, + recommendationEffectInterval: recommendationEffectInterval + ) + + // Carb entries (including a pending manual-bolus entry) can extend back to carbScheduleStart, and + // CarbMath.map(to:) preconditionFailures if the ISF/carb-ratio timelines don't cover every + // carb entry's start date. timelineIntervalForSensitivity derives its window from dose and + // glucose history only — which can be more recent than carbsStart (e.g. after a CGM gap) — + // so extend the ISF (and override) window back to cover every entry, matching carbRatio. + let sensitivityStart = min(neededSensitivityTimeline.start, carbScheduleStart) + + let sensitivity = try await settingsProvider.getInsulinSensitivityHistory( + startDate: sensitivityStart, + endDate: neededSensitivityTimeline.end + ) + + let dosingLimits = try await settingsProvider.getDosingLimits(at: baseTime) + + guard let maxBolus = dosingLimits.maxBolus else { + throw LoopError.configurationError(.maximumBolus) + } + + guard let maxBasalRate = dosingLimits.maxBasalRate else { + throw LoopError.configurationError(.maximumBasalRatePerHour) + } + + var overrides = overrideHistory.getOverrideHistory(startDate: sensitivityStart, endDate: forecastEndTime) + + // For recommendation, we should consider preMeal override to be ending at time of dose + if presumePresetEndingNow, + let activeOverride, + let index = overrides.lastIndex(of: activeOverride) { + overrides[index].scheduledEndDate = baseTime + } + + guard !sensitivity.isEmpty else { + throw LoopError.configurationError(.insulinSensitivitySchedule) + } + + let sensitivityWithOverrides = overrides.applySensitivity(over: sensitivity) + + guard !basal.isEmpty else { + throw LoopError.configurationError(.basalRateSchedule) + } + let basalWithOverrides = overrides.applyBasal(over: basal) + + guard !carbRatio.isEmpty else { + throw LoopError.configurationError(.carbRatioSchedule) + } + let carbRatioWithOverrides = overrides.applyCarbRatio(over: carbRatio) + + + var target: [LoopKit.AbsoluteScheduleValue>] + + guard var suspendThreshold = dosingLimits.suspendThreshold else { + throw LoopError.configurationError(.suspendThreshold) + } + + // If we have an active override, and it's not a preMeal override that should be disabled, + // or ended for other reasons (like comparing effects without preset), then override the + // target for the entire forecast. + if let activeOverride, + !presumePresetEndingNow + { + guard let schedule = settingsProvider.settings.glucoseTargetRangeSchedule else + { + throw LoopError.configurationError(.glucoseTargetRangeSchedule) + } + let scheduledRange = schedule.quantityRange(at: baseTime) + let overriddenTargetRange = activeOverride.effectiveCorrectionRangeDuring(scheduledRange: scheduledRange) + target = [ + LoopKit.AbsoluteScheduleValue( + startDate: baseTime, + endDate: forecastEndTime, + value: overriddenTargetRange + ) + ] + + if activeOverride.veryHighInsulinNeeds { + suspendThreshold = max(TemporaryScheduleOverride.highInsulinNeedsMitigationCorrectionRangeLimit, suspendThreshold) + } + + } else { + target = try await settingsProvider.getTargetRangeHistory(startDate: baseTime, endDate: forecastEndTime) + } + + guard !target.isEmpty else { + throw LoopError.configurationError(.glucoseTargetRangeSchedule) + } + + + return StoredDataAlgorithmInput( + glucoseHistory: glucose, + doses: dosesWithModel, + carbEntries: carbEntries, + predictionStart: baseTime, + basal: basalWithOverrides.algorithmSchedule(), + sensitivityMgdlPerUnit: sensitivityWithOverrides.algorithmMgdlSchedule(), + carbRatio: carbRatioWithOverrides.algorithmSchedule(), + targetMgdl: target.algorithmMgdlRanges(), + suspendThresholdMgdl: suspendThreshold.doubleValue(for: .milligramsPerDeciliter), + maxBolus: maxBolus, + maxBasalRate: maxBasalRate, + useIntegralRetrospectiveCorrection: false, + includePositiveVelocityAndRC: true, + carbAbsorptionModel: carbAbsorptionModel, + recommendationInsulinModel: AlgorithmInsulinModel(recommendationInsulinModel), + recommendationType: .manualBolus, + automaticBolusApplicationFactor: nil, + experiments: experiments, + volatilityBaseline: volatilityBaseline) + } +} + +public extension AlgorithmExperiment { + /// `LoopDataManager.algorithmExperiments(for:output:)`'s body, shared with the watch: the + /// enabled experiments as a dosing decision records them. Glucose based partial application only + /// affects automatic boluses, so it is in effect only with that strategy. + static func records(enabled: Set, dosingStrategy: AutomaticDosingStrategy, + volatilityBaseline: Double?, output: AlgorithmOutput? = nil) -> [AlgorithmExperiment] { + enabled + .filter { $0 != .glucoseBasedApplicationFactor || dosingStrategy == .automaticBolus } + .sorted { $0.rawValue < $1.rawValue } + .map { experiment in + switch experiment { + case .guardAndRunway: + return guardAndRunwayRecord(output?.guardAndRunway, volatilityBaseline: volatilityBaseline) + case .slowRetrospectiveCorrection: + var parameters: [String: JSONValue] = [:] + if let slow = output?.slowRetrospectiveCorrection { + parameters["level"] = .number(slow.level) + parameters["multiplier"] = .number(slow.multiplier) + } + return AlgorithmExperiment(identifier: experiment.rawValue, parameters: parameters.isEmpty ? nil : .object(parameters)) + default: + return AlgorithmExperiment(identifier: experiment.rawValue) + } + } + } + + private static func guardAndRunwayRecord(_ guardAndRunway: GuardAndRunwayOutput?, volatilityBaseline: Double?) -> AlgorithmExperiment { + var parameters: [String: JSONValue] = ["version": .number(2)] + if let volatilityBaseline { + parameters["volatilityBaseline"] = .number(volatilityBaseline) + } + if let guardAndRunway { + parameters["volatility"] = .number(guardAndRunway.volatility) + parameters["bandApplied"] = .bool(guardAndRunway.bandApplied) + parameters["bandGain"] = .number(guardAndRunway.bandGain) + parameters["bandDepth"] = .number(guardAndRunway.bandDepth) + parameters["runwayShift"] = .number(guardAndRunway.runwayShift) + parameters["postLowRiseCutActive"] = .bool(guardAndRunway.postLowRiseCutActive) + } + return AlgorithmExperiment(identifier: ExperimentOption.guardAndRunway.rawValue, parameters: .object(parameters)) + } +} + +public extension PumpManagerStatus.BasalDeliveryState { + var currentTempBasal: DoseEntry? { + switch self { + case .tempBasal(let dose): + return dose + default: + return nil + } + } + + func currentBasalRate(currentScheduledBasalRate: Double) -> Double? { + switch self { + case .tempBasal(let dose): + return dose.unitsPerHour + case .suspended: + return 0 + case .pumpInoperable: + return nil + default: + return currentScheduledBasalRate + } + } +} + +public extension StoredDataAlgorithmInput { + /// `LoopDataManager.loop()`'s checks on its input, before the algorithm runs. + func checkRecency(at loopBaseTime: Date, lastAddedPumpData: Date) throws { + guard let latestGlucose = glucoseHistory.last else { + throw LoopError.missingDataError(.glucose) + } + + guard loopBaseTime.timeIntervalSince(latestGlucose.startDate) <= LoopCoreConstants.inputDataRecencyInterval else { + throw LoopError.glucoseTooOld(date: latestGlucose.startDate) + } + + guard latestGlucose.startDate.timeIntervalSince(loopBaseTime) <= LoopCoreConstants.inputDataRecencyInterval else { + throw LoopError.invalidFutureGlucose(date: latestGlucose.startDate) + } + + guard loopBaseTime.timeIntervalSince(lastAddedPumpData) <= LoopCoreConstants.inputDataRecencyInterval else { + throw LoopError.pumpDataTooOld(date: lastAddedPumpData) + } + } +} diff --git a/LoopCore/Managers/LoopCycle.swift b/LoopCore/Managers/LoopCycle.swift new file mode 100644 index 0000000000..72f0c84e70 --- /dev/null +++ b/LoopCore/Managers/LoopCycle.swift @@ -0,0 +1,531 @@ +// +// LoopCycle.swift +// Loop +// +// One cycle of the dosing loop, as both apps run it: the algorithm's input, its recommendation, +// the dose as the pump will take it, the checks before sending it, the send, and the cycle's +// dosing decision. LoopDataManager runs it on the phone; the watch's loop runs it while the watch +// holds the pump. What only the device running it knows comes from its `LoopCycleHost`. +// + +import Foundation +import LoopKit +import LoopAlgorithm +import os.log + +public protocol DeliveryDelegate: AnyObject { + var isSuspended: Bool { get } + var isManualTempBasalRunning: Bool { get } + var pumpInsulinType: InsulinType? { get } + var basalDeliveryState: PumpManagerStatus.BasalDeliveryState? { get } + var isPumpConfigured: Bool { get } + var pumpManagerStatus: PumpManagerStatus? { get } + var pumpStatusHighlight: DeviceStatusHighlight? { get } + var cgmManagerStatus: CGMManagerStatus? { get } + + func enact(bolus: Double?, tempBasal: LoopKit.TempBasalRecommendation?, decisionId: UUID?) async throws + func enactBolus(units: Double, decisionId: UUID?, activationType: BolusActivationType) async throws + func roundBasalRate(unitsPerHour: Double) -> Double + func roundBolusVolume(units: Double) -> Double +} + +/// A delivery delegate over the pump manager it holds: the phone's `DeviceDataManager`, and the +/// watch over a lent pod. Its delivery members are the ones below; sending a dose that passed their +/// checks, a pump status highlight and a manual bolus are each device's own. +public protocol PumpManagerDeliveryDelegate: DeliveryDelegate { + var pumpManager: PumpManager? { get } + var cgmManager: CGMManager? { get } + + /// Sends a dose `enact` has checked, logged the device's way. + func send(bolus: Double?, tempBasal: LoopKit.TempBasalRecommendation?, decisionId: UUID?, to pumpManager: PumpManager) async throws +} + +public extension PumpManagerDeliveryDelegate { + var isPumpConfigured: Bool { + return pumpManager != nil + } + + func roundBasalRate(unitsPerHour: Double) -> Double { + guard let pumpManager = pumpManager else { + return unitsPerHour + } + + return pumpManager.roundToSupportedBasalRate(unitsPerHour: unitsPerHour) + } + + func roundBolusVolume(units: Double) -> Double { + guard let pumpManager = pumpManager else { + return units + } + + return pumpManager.roundToSupportedBolusVolume(units: units) + } + + var pumpInsulinType: InsulinType? { + return pumpManager?.status.insulinType + } + + var isSuspended: Bool { + return pumpManager?.status.basalDeliveryState?.isSuspended ?? false + } + + var isManualTempBasalRunning: Bool { + guard case .tempBasal(let dose) = pumpManager?.status.basalDeliveryState else { + return false + } + return dose.automatic == false && dose.endDate > Date() + } + + var basalDeliveryState: PumpManagerStatus.BasalDeliveryState? { + return pumpManager?.status.basalDeliveryState + } + + var pumpManagerStatus: PumpManagerStatus? { + return pumpManager?.status + } + + var cgmManagerStatus: CGMManagerStatus? { + return cgmManager?.cgmManagerStatus + } + + /// Refused while the pump's last command is unconfirmed: its driver settles it at its next session. + func enact(bolus: Double?, tempBasal: LoopKit.TempBasalRecommendation?, decisionId: UUID?) async throws { + guard let pumpManager = pumpManager else { + throw LoopError.configurationError(.pumpManager) + } + + guard !pumpManager.status.deliveryIsUncertain else { + throw LoopError.connectionError + } + + try await send(bolus: bolus, tempBasal: tempBasal, decisionId: decisionId, to: pumpManager) + } +} + +public enum CancelActiveTempBasalReason: String { + case automaticDosingDisabled + case unreliableCGMData + case maximumBasalRateChanged + case pumpControlReturned + case pumpControlReleased +} + +/// What a loop cycle needs from the device running it. +public protocol LoopCycleHost: AnyObject { + var deliveryDelegate: DeliveryDelegate? { get } + + var overrideHistory: TemporaryScheduleOverrideHistory { get } + /// The override in effect now, if any. + var activeOverride: TemporaryScheduleOverride? { get } + + var enabledAlgorithmExperiments: Set { get } + /// Guard + runway's volatility baseline; asked only while that experiment is enabled. + func currentVolatilityBaseline(at date: Date) async -> Double? + func algorithmExperiments(for dosingStrategy: AutomaticDosingStrategy, output: AlgorithmOutput?) -> [AlgorithmExperiment] + + var currentDosingStrategy: AutomaticDosingStrategy { get } + var usePositiveMomentumAndRCForManualBoluses: Bool { get } + var carbAbsorptionModel: CarbAbsorptionModel { get } + func insulinModel(for type: InsulinType?) -> LoopKit.InsulinModel + + /// This device holds the pump: it may command it, and its dosing decisions are the record. + var holdsPump: Bool { get } + /// The device's loop is closed: its cycles may dose automatically while it holds the pump. + var isClosedLoop: Bool { get } + + /// The device's battery, for the dosing decisions it stores. + var controllerStatus: StoredDosingDecision.ControllerStatus { get } +} + +public final class LoopCycle { + /// What one cycle computed and did. + public struct Outcome { + public var dosingDecision: StoredDosingDecision + public var input: StoredDataAlgorithmInput? + public var output: AlgorithmOutput? + /// The automatic recommendation as it was to be enacted: rounded and adjusted for the + /// pump's current temp. + public var recommendation: LoopKit.AutomaticDoseRecommendation? + /// The temp basal command; nil when the pump's current temp already serves. + public var basalAdjustment: LoopKit.TempBasalRecommendation? + /// The dose went to the pump and the pump took it. + public var enacted = false + public var error: LoopError? + } + + public let doseStore: DoseStoreProtocol + public let carbStore: CarbStoreProtocol + public let glucoseStore: GlucoseStoreProtocol + /// Nil only in suites that do not look at decisions. + public let dosingDecisionStore: DosingDecisionStoreProtocol? + public let settingsProvider: SettingsProvider + public weak var host: LoopCycleHost? + + private let now: () -> Date + private let log = OSLog(subsystem: "com.loopkit.Loop", category: "LoopCycle") + + /// The manual recommendation in the last stored remote recommendation. + private var lastManualBolusRecommendation: LoopKit.ManualBolusRecommendation? + + public init(doseStore: DoseStoreProtocol, carbStore: CarbStoreProtocol, glucoseStore: GlucoseStoreProtocol, + dosingDecisionStore: DosingDecisionStoreProtocol?, settingsProvider: SettingsProvider, + host: LoopCycleHost?, now: @escaping () -> Date) { + self.doseStore = doseStore + self.carbStore = carbStore + self.glucoseStore = glucoseStore + self.dosingDecisionStore = dosingDecisionStore + self.settingsProvider = settingsProvider + self.host = host + self.now = now + } + + private var requiredHost: LoopCycleHost { + guard let host else { preconditionFailure("A loop cycle runs for a host.") } + return host + } + + public func fetchData( + for baseTime: Date? = nil, + presumePresetEndingNow: Bool = false, + ensureDosingCoverageStart: Date? = nil, + ensureCarbCoverageStart: Date? = nil, + projectOngoingDoses: Bool = false, + isolation: isolated (any Actor)? = #isolation + ) async throws -> StoredDataAlgorithmInput { + let host = requiredHost + let now = self.now() + let experiments = host.enabledAlgorithmExperiments + let volatilityBaseline = experiments.contains(.guardAndRunway) ? await host.currentVolatilityBaseline(at: baseTime ?? now) : nil + return try await StoredDataAlgorithmInput.fetch( + for: baseTime, + presumePresetEndingNow: presumePresetEndingNow, + ensureDosingCoverageStart: ensureDosingCoverageStart, + ensureCarbCoverageStart: ensureCarbCoverageStart, + projectOngoingDoses: projectOngoingDoses, + now: now, + doseStore: doseStore, + carbStore: carbStore, + glucoseStore: glucoseStore, + settingsProvider: settingsProvider, + overrideHistory: host.overrideHistory, + activeOverride: host.activeOverride, + pumpInsulinType: host.deliveryDelegate?.pumpInsulinType, + insulinModel: host.insulinModel(for:), + experiments: experiments, + volatilityBaseline: volatilityBaseline, + carbAbsorptionModel: host.carbAbsorptionModel) + } + + /// One cycle at `loopBaseTime`. The decision is stored, on both arms, by a host that holds the pump. + public func run(at loopBaseTime: Date, isolation: isolated (any Actor)? = #isolation) async -> Outcome { + let host = requiredHost + var outcome = Outcome(dosingDecision: StoredDosingDecision( + date: loopBaseTime, + reason: "loop", + settings: StoredDosingDecision.Settings(settingsProvider.settings) + )) + + do { + guard let deliveryDelegate = host.deliveryDelegate else { + preconditionFailure("Unable to dose without dosing delegate.") + } + + var input = try await fetchData(for: loopBaseTime) + + // Trim future basal + input.doses = input.doses.trimmed(to: loopBaseTime) + + let dosingStrategy = host.currentDosingStrategy + input.recommendationType = dosingStrategy.recommendationType + outcome.dosingDecision.algorithmExperiments = host.algorithmExperiments(for: dosingStrategy, output: nil) + outcome.input = input + + try input.checkRecency(at: loopBaseTime, lastAddedPumpData: doseStore.lastAddedPumpData) + + var output = Algorithm.run(input: input) + outcome.output = output + outcome.dosingDecision.algorithmExperiments = host.algorithmExperiments(for: dosingStrategy, output: output) + + switch output.recommendationResult { + case .success(let recommendation): + // Round delivery amounts to pump supported amounts, + // And determine if a change in dosing should be made. + + let algoRecommendation = recommendation.automatic! + os_log("Algorithm recommendation: %{public}@", log: log, type: .default, String(describing: algoRecommendation)) + + var recommendationToEnact = algoRecommendation + // Round bolus recommendation based on pump bolus precision + if let bolus = algoRecommendation.bolusUnits, bolus > 0 { + recommendationToEnact.bolusUnits = deliveryDelegate.roundBolusVolume(units: bolus) + } + + var basal = algoRecommendation.basalAdjustment + + basal.unitsPerHour = deliveryDelegate.roundBasalRate(unitsPerHour: basal.unitsPerHour) + + let scheduledBasalRate = input.basal.closestPrior(to: loopBaseTime)!.value + let activeOverride = host.overrideHistory.activeOverride(at: loopBaseTime) + + let basalAdjustment = basal.loopKit.adjustForCurrentDelivery( + at: loopBaseTime, + neutralBasalRate: scheduledBasalRate, + currentTempBasal: deliveryDelegate.basalDeliveryState?.currentTempBasal, + continuationInterval: .minutes(11), + neutralBasalRateMatchesPump: activeOverride == nil + ) + + if let basalAdjustment { + recommendationToEnact.basalAdjustment = basalAdjustment.algorithm + } + + output.recommendationResult = .success(.init(automatic: recommendationToEnact)) + outcome.output = output + outcome.recommendation = recommendationToEnact.loopKit + outcome.basalAdjustment = basalAdjustment + + if recommendationToEnact != algoRecommendation { + os_log("Recommendation changed to: %{public}@", log: log, type: .default, String(describing: recommendationToEnact)) + } + + outcome.dosingDecision.updateFrom(input: input, output: output) + + if host.isClosedLoop, host.holdsPump { + if deliveryDelegate.basalDeliveryState == .pumpInoperable { + throw LoopError.pumpInoperable + } + + if deliveryDelegate.isSuspended { + throw LoopError.pumpSuspended + } + + if deliveryDelegate.isManualTempBasalRunning { + throw LoopError.manualTempBasalRunning + } + + os_log("Enacting: %{public}@", log: log, type: .default, String(describing: recommendationToEnact)) + + try await deliveryDelegate.enact(bolus: recommendationToEnact.bolusUnits, tempBasal: basalAdjustment, decisionId: outcome.dosingDecision.id) + + outcome.enacted = true + outcome.dosingDecision.enactedTempBasal = basalAdjustment + outcome.dosingDecision.enactedBolusAmount = recommendationToEnact.bolusUnits + } else { + os_log("Not adjusting dosing during open loop.", log: log, type: .default) + } + + case .failure(let error): + throw error + } + } catch { + os_log("loop() did error: %{public}@", log: log, type: .error, String(describing: error)) + let loopError = error as? LoopError ?? .unknownError(error) + outcome.error = loopError + outcome.dosingDecision.appendError(loopError) + } + + if host.holdsPump { + await dosingDecisionStore?.storeDosingDecision(outcome.dosingDecision) + } + return outcome + } + + /// The manual bolus recommendation's input: the active preset presumed ending now when asked, or + /// when a pre-meal preset meets a carb entry; no trim, since a bolus goes on top of a temp that + /// keeps running. + public func manualBolusInput( + manualGlucoseSample: NewGlucoseSample? = nil, + carbEntryChange: CarbEntryChange? = nil, + truncatingActiveOverride: Bool = false, + isolation: isolated (any Actor)? = #isolation + ) async throws -> StoredDataAlgorithmInput { + let host = requiredHost + var endingPremealOverride = false + + if carbEntryChange != nil, + let activeOverride = host.activeOverride, + activeOverride.context == .preMeal + { + endingPremealOverride = true + } + + var input = try await fetchData( + for: now(), + presumePresetEndingNow: truncatingActiveOverride || endingPremealOverride, + ensureCarbCoverageStart: carbEntryChange?.startDate + ) + .addingGlucoseSample(sample: manualGlucoseSample?.asStoredGlucoseSample) + .removingCarbEntry(carbEntry: carbEntryChange?.originalEntry) + .addingCarbEntry(carbEntry: carbEntryChange?.asStoredCarbEntry) + + input.includePositiveVelocityAndRC = host.usePositiveMomentumAndRCForManualBoluses + input.recommendationType = .manualBolus + return input + } + + public func recommendManualBolus( + manualGlucoseSample: NewGlucoseSample? = nil, + carbEntryChange: CarbEntryChange? = nil, + truncatingActiveOverride: Bool = false, + isolation: isolated (any Actor)? = #isolation + ) async throws -> LoopKit.ManualBolusRecommendation? { + let input = try await manualBolusInput(manualGlucoseSample: manualGlucoseSample, carbEntryChange: carbEntryChange, + truncatingActiveOverride: truncatingActiveOverride) + + try input.validateCarbScheduleCoverage() + let output = Algorithm.run(input: input) + + switch output.recommendationResult { + case .success(let prediction): + guard var manualBolusRecommendation = prediction.manual else { return nil } + if let roundedAmount = requiredHost.deliveryDelegate?.roundBolusVolume(units: manualBolusRecommendation.amount) { + manualBolusRecommendation.amount = roundedAmount + } + return manualBolusRecommendation.loopKit + case .failure(let error): + throw error + } + } + + /// A manual bolus's decision as stored, as of `date`, with the settings, device status and + /// experiments it was made under. Nil without a host. The host stores it. + public func manualBolusDosingDecision(_ bolusDosingDecision: BolusDosingDecision, withDate date: Date) -> StoredDosingDecision? { + guard let host else { return nil } + var dosingDecision = StoredDosingDecision(id: bolusDosingDecision.id, + date: date, + reason: bolusDosingDecision.reason.rawValue, + settings: StoredDosingDecision.Settings(settingsProvider.settings), + scheduleOverride: bolusDosingDecision.scheduleOverride, + controllerStatus: host.controllerStatus, + pumpManagerStatus: host.deliveryDelegate?.pumpManagerStatus, + cgmManagerStatus: host.deliveryDelegate?.cgmManagerStatus, + lastReservoirValue: StoredDosingDecision.LastReservoirValue(doseStore.lastReservoirValue), + historicalGlucose: bolusDosingDecision.historicalGlucose, + originalCarbEntry: bolusDosingDecision.originalCarbEntry, + carbEntry: bolusDosingDecision.carbEntry, + manualGlucoseSample: bolusDosingDecision.manualGlucoseSample, + carbsOnBoard: bolusDosingDecision.carbsOnBoard, + insulinOnBoard: bolusDosingDecision.insulinOnBoard, + glucoseTargetRangeSchedule: bolusDosingDecision.glucoseTargetRangeSchedule, + predictedGlucose: bolusDosingDecision.predictedGlucose, + manualBolusRecommendation: bolusDosingDecision.manualBolusRecommendation, + manualBolusRequested: bolusDosingDecision.manualBolusRequested) + dosingDecision.algorithmExperiments = host.algorithmExperiments(for: host.currentDosingStrategy, output: nil) + return dosingDecision + } + + /// Cancels the running temp if the loop set it, for `reason`, and stores the decision. False when + /// there is nothing to cancel, or this device does not hold the pump (the controller holding it + /// owns the program). A failure stores nothing. The hooks bracket the send. + public func cancelActiveTempBasal( + for reason: CancelActiveTempBasalReason, + dosingStarted: (LoopKit.AutomaticDoseRecommendation) -> Void = { _ in }, + dosingFinished: () -> Void = {}, + isolation: isolated (any Actor)? = #isolation + ) async throws -> Bool { + let host = requiredHost + guard let deliveryDelegate = host.deliveryDelegate, + case .tempBasal(let dose) = deliveryDelegate.basalDeliveryState, (dose.automatic ?? true) else { return false } + + guard host.holdsPump else { + os_log("Temp-basal cancel SKIPPED (%{public}@) — this device does not hold the pump", log: log, type: .default, reason.rawValue) + return false + } + + os_log("Cancelling active temp basal for reason: %{public}@", log: log, type: .default, reason.rawValue) + + let recommendation = LoopKit.AutomaticDoseRecommendation(basalAdjustment: .cancel, direction: .decrease) + + var dosingDecision = StoredDosingDecision(date: now(), reason: reason.rawValue) + dosingDecision.settings = StoredDosingDecision.Settings(settingsProvider.settings) + dosingDecision.automaticDoseRecommendation = recommendation + + dosingStarted(recommendation) + try await deliveryDelegate.enact(bolus: recommendation.bolusUnits, tempBasal: recommendation.basalAdjustment, decisionId: dosingDecision.id) + dosingFinished() + + await dosingDecisionStore?.storeDosingDecision(dosingDecision) + return true + } + + /// The "updateRemoteRecommendation" decision for a display run: stored when forced, or when the + /// manual recommendation changed, by a host that holds the pump. + /// DIY: these decisions are also Nightscout's Loop pill and forecast source (NightscoutService pairs + /// them with the cached "loop" decision), so callers force-store after every cycle and temp basal + /// cancel, to keep Nightscout current even when the manual recommendation has not changed. + public func storeRemoteRecommendation(displayState: AlgorithmDisplayState, force: Bool, isolation: isolated (any Actor)? = #isolation) async { + let host = requiredHost + guard host.holdsPump else { return } + + if lastManualBolusRecommendation == nil { + lastManualBolusRecommendation = displayState.output?.recommendation?.manual?.loopKit + } + + let recommendationChanged = lastManualBolusRecommendation != displayState.output?.recommendation?.manual?.loopKit + + guard force || recommendationChanged else { + return + } + + lastManualBolusRecommendation = displayState.output?.recommendation?.manual?.loopKit + + guard let output = displayState.output else { return } + + var dosingDecision = StoredDosingDecision(date: now(), reason: "updateRemoteRecommendation") + dosingDecision.algorithmExperiments = host.algorithmExperiments(for: host.currentDosingStrategy, output: output) + dosingDecision.predictedGlucose = output.predictedGlucose.map(\.loopKit) + dosingDecision.insulinOnBoard = displayState.activeInsulin + dosingDecision.carbsOnBoard = displayState.activeCarbs + switch output.recommendationResult { + case .success(let recommendation): + dosingDecision.automaticDoseRecommendation = recommendation.automatic?.loopKit + if let recommendationDate = displayState.input?.predictionStart, let manualRec = recommendation.manual { + dosingDecision.manualBolusRecommendation = ManualBolusRecommendationWithDate(recommendation: manualRec.loopKit, date: recommendationDate) + } + case .failure(let error): + if let loopError = error as? LoopError { + dosingDecision.errors.append(loopError.issue) + } else { + dosingDecision.errors.append(.init(id: "error", details: ["description": error.localizedDescription])) + } + } + + dosingDecision.controllerStatus = host.controllerStatus + + // Device status for the Nightscout devicestatus.pump payload. + dosingDecision.pumpManagerStatus = host.deliveryDelegate?.pumpManagerStatus + if let pumpStatusHighlight = host.deliveryDelegate?.pumpStatusHighlight { + dosingDecision.pumpStatusHighlight = StoredDosingDecision.StoredDeviceHighlight( + localizedMessage: pumpStatusHighlight.localizedMessage, + imageName: pumpStatusHighlight.imageName, + state: pumpStatusHighlight.state) + } + dosingDecision.cgmManagerStatus = host.deliveryDelegate?.cgmManagerStatus + dosingDecision.lastReservoirValue = StoredDosingDecision.LastReservoirValue(doseStore.lastReservoirValue) + + os_log("Manual bolus rec = %{public}@", log: log, type: .debug, String(describing: dosingDecision.manualBolusRecommendation)) + await dosingDecisionStore?.storeDosingDecision(dosingDecision) + } + + /// The display run: the forecast and recommendations shown between cycles, over a day of doses so + /// the last manual bolus can be read from its input. Empty when the input cannot be fetched. + public func displayState(isolation: isolated (any Actor)? = #isolation) async -> AlgorithmDisplayState { + var state = AlgorithmDisplayState() + do { + let now = self.now() + var input = try await fetchData(for: now, ensureDosingCoverageStart: now.addingTimeInterval(.days(-1)), projectOngoingDoses: true) + input.recommendationType = .manualBolus + state.input = input + state.output = await Self.runAlgorithm(input: input) + } catch { + let loopError = error as? LoopError ?? .unknownError(error) + os_log("Error updating Loop state: %{public}@", log: log, type: .error, String(describing: loopError)) + } + return state + } + + /// Off the caller's actor. + private static func runAlgorithm(input: StoredDataAlgorithmInput) async -> AlgorithmOutput { + Algorithm.run(input: input) + } +} diff --git a/LoopCore/Managers/SettingsProvider.swift b/LoopCore/Managers/SettingsProvider.swift new file mode 100644 index 0000000000..06c82811a2 --- /dev/null +++ b/LoopCore/Managers/SettingsProvider.swift @@ -0,0 +1,22 @@ +// +// SettingsProvider.swift +// Loop +// +// Extracted from SettingsManager so a watch can serve therapy settings from its grant snapshot. +// + +import Foundation +import LoopKit +import Observation + +public protocol SettingsProvider: Observable { + var settings: StoredSettings { get } + var dosingEnabled: Bool { get } + + func getBasalHistory(startDate: Date, endDate: Date) async throws -> [AbsoluteScheduleValue] + func getCarbRatioHistory(startDate: Date, endDate: Date) async throws -> [AbsoluteScheduleValue] + func getInsulinSensitivityHistory(startDate: Date, endDate: Date) async throws -> [AbsoluteScheduleValue] + func getTargetRangeHistory(startDate: Date, endDate: Date) async throws -> [AbsoluteScheduleValue>] + func getDosingLimits(at date: Date) async throws -> DosingLimits + func executeSettingsQuery(fromQueryAnchor queryAnchor: SettingsStore.QueryAnchor?, limit: Int, completion: @escaping (SettingsStore.SettingsQueryResult) -> Void) +} diff --git a/Loop/Managers/Store Protocols/CarbStoreProtocol.swift b/LoopCore/Managers/Store Protocols/CarbStoreProtocol.swift similarity index 90% rename from Loop/Managers/Store Protocols/CarbStoreProtocol.swift rename to LoopCore/Managers/Store Protocols/CarbStoreProtocol.swift index 7b0006b57f..a9cb53f688 100644 --- a/Loop/Managers/Store Protocols/CarbStoreProtocol.swift +++ b/LoopCore/Managers/Store Protocols/CarbStoreProtocol.swift @@ -8,7 +8,7 @@ import LoopKit -protocol CarbStoreProtocol: AnyObject { +public protocol CarbStoreProtocol: AnyObject { func getCarbEntries(start: Date?, end: Date?, dateAscending: Bool, fetchLimit: Int?, with favoriteFoodID: String?) async throws -> [StoredCarbEntry] @@ -20,7 +20,7 @@ protocol CarbStoreProtocol: AnyObject { } -extension CarbStoreProtocol { +public extension CarbStoreProtocol { func getCarbEntries(start: Date?, end: Date?) async throws -> [StoredCarbEntry] { try await getCarbEntries(start: start, end: end, dateAscending: true, fetchLimit: nil, with: nil) } diff --git a/Loop/Managers/Store Protocols/DoseStoreProtocol.swift b/LoopCore/Managers/Store Protocols/DoseStoreProtocol.swift similarity index 91% rename from Loop/Managers/Store Protocols/DoseStoreProtocol.swift rename to LoopCore/Managers/Store Protocols/DoseStoreProtocol.swift index 6de9aa59b2..54ffbeeebb 100644 --- a/Loop/Managers/Store Protocols/DoseStoreProtocol.swift +++ b/LoopCore/Managers/Store Protocols/DoseStoreProtocol.swift @@ -9,7 +9,7 @@ import LoopKit import HealthKit -protocol DoseStoreProtocol: AnyObject { +public protocol DoseStoreProtocol: AnyObject { func getNormalizedDoseEntries(start: Date, end: Date?) async throws -> [DoseEntry] func addDoses(_ doses: [DoseEntry], from device: HKDevice?) async throws diff --git a/Loop/Managers/Store Protocols/DosingDecisionStoreProtocol.swift b/LoopCore/Managers/Store Protocols/DosingDecisionStoreProtocol.swift similarity index 61% rename from Loop/Managers/Store Protocols/DosingDecisionStoreProtocol.swift rename to LoopCore/Managers/Store Protocols/DosingDecisionStoreProtocol.swift index 7451b237a2..6102b88112 100644 --- a/Loop/Managers/Store Protocols/DosingDecisionStoreProtocol.swift +++ b/LoopCore/Managers/Store Protocols/DosingDecisionStoreProtocol.swift @@ -8,17 +8,17 @@ import LoopKit -struct LightDosingDecision: DosingDecision { - let automaticDoseRecommendation: AutomaticDoseRecommendation? - let carbEntry: StoredCarbEntry? - let id: UUID - let manualBolusRecommendation: ManualBolusRecommendationWithDate? - let manualBolusRequested: Double? - let scheduleOverride: TemporaryScheduleOverride? - let syncIdentifier: UUID +public struct LightDosingDecision: DosingDecision { + public let automaticDoseRecommendation: AutomaticDoseRecommendation? + public let carbEntry: StoredCarbEntry? + public let id: UUID + public let manualBolusRecommendation: ManualBolusRecommendationWithDate? + public let manualBolusRequested: Double? + public let scheduleOverride: TemporaryScheduleOverride? + public let syncIdentifier: UUID } -protocol DosingDecisionStoreProtocol: CriticalEventLog { +public protocol DosingDecisionStoreProtocol: CriticalEventLog { var delegate: DosingDecisionStoreDelegate? { get set } func storeDosingDecision(_ dosingDecision: StoredDosingDecision) async diff --git a/Loop/Managers/Store Protocols/GlucoseStoreProtocol.swift b/LoopCore/Managers/Store Protocols/GlucoseStoreProtocol.swift similarity index 90% rename from Loop/Managers/Store Protocols/GlucoseStoreProtocol.swift rename to LoopCore/Managers/Store Protocols/GlucoseStoreProtocol.swift index c1600d165b..0a8609dad8 100644 --- a/Loop/Managers/Store Protocols/GlucoseStoreProtocol.swift +++ b/LoopCore/Managers/Store Protocols/GlucoseStoreProtocol.swift @@ -8,7 +8,7 @@ import LoopKit -protocol GlucoseStoreProtocol: AnyObject { +public protocol GlucoseStoreProtocol: AnyObject { var latestGlucose: GlucoseSampleValue? { get } func getGlucoseSamples(start: Date?, end: Date?) async throws -> [StoredGlucoseSample] func addGlucoseSamples(_ samples: [NewGlucoseSample]) async throws -> [StoredGlucoseSample] diff --git a/Loop/Models/AlgorithmBridge.swift b/LoopCore/Models/AlgorithmBridge.swift similarity index 70% rename from Loop/Models/AlgorithmBridge.swift rename to LoopCore/Models/AlgorithmBridge.swift index 53ded55f37..5b7eff2999 100644 --- a/Loop/Models/AlgorithmBridge.swift +++ b/LoopCore/Models/AlgorithmBridge.swift @@ -1,6 +1,8 @@ // // AlgorithmBridge.swift -// Loop +// LoopCore +// +// In LoopCore so the phone and the watch (which runs the loop during Sport Mode) convert through one bridge. // // Copyright © 2026 LoopKit Authors. All rights reserved. // @@ -8,7 +10,6 @@ import Foundation import LoopKit import LoopAlgorithm -import LoopKitUI // LoopKit's stored values, given to LoopAlgorithm in its fixed units. @@ -25,44 +26,44 @@ extension StoredGlucoseSample: @retroactive LoopAlgorithm.GlucoseSampleValue { // These types conform to both modules' TimelineValue, so the protocols' shared helpers are // ambiguous on them; these concrete versions take precedence. extension Array where Element == StoredGlucoseSample { - func filterDateRange(_ startDate: Date?, _ endDate: Date?) -> [StoredGlucoseSample] { + public func filterDateRange(_ startDate: Date?, _ endDate: Date?) -> [StoredGlucoseSample] { filter { (startDate == nil || $0.endDate >= startDate!) && (endDate == nil || $0.startDate <= endDate!) } } } extension Array where Element == StoredCarbEntry { - func filterDateRange(_ startDate: Date?, _ endDate: Date?) -> [StoredCarbEntry] { + public func filterDateRange(_ startDate: Date?, _ endDate: Date?) -> [StoredCarbEntry] { filter { (startDate == nil || $0.endDate >= startDate!) && (endDate == nil || $0.startDate <= endDate!) } } } -struct AlgorithmInsulinModel: LoopAlgorithm.InsulinModel { - let model: LoopKit.InsulinModel +public struct AlgorithmInsulinModel: LoopAlgorithm.InsulinModel { + public let model: LoopKit.InsulinModel - init(_ model: LoopKit.InsulinModel) { + public init(_ model: LoopKit.InsulinModel) { self.model = model } - func percentEffectRemaining(at time: TimeInterval) -> Double { + public func percentEffectRemaining(at time: TimeInterval) -> Double { model.percentEffectRemaining(at: time) } - var effectDuration: TimeInterval { model.effectDuration } + public var effectDuration: TimeInterval { model.effectDuration } - var delay: TimeInterval { model.delay } + public var delay: TimeInterval { model.delay } } extension Array { - func algorithmSchedule() -> [LoopAlgorithm.AbsoluteScheduleValue] where Element == LoopKit.AbsoluteScheduleValue { + public func algorithmSchedule() -> [LoopAlgorithm.AbsoluteScheduleValue] where Element == LoopKit.AbsoluteScheduleValue { map { LoopAlgorithm.AbsoluteScheduleValue(startDate: $0.startDate, endDate: $0.endDate, value: $0.value) } } /// Glucose quantities (or sensitivities) in mg/dL - func algorithmMgdlSchedule() -> [LoopAlgorithm.AbsoluteScheduleValue] where Element == LoopKit.AbsoluteScheduleValue { + public func algorithmMgdlSchedule() -> [LoopAlgorithm.AbsoluteScheduleValue] where Element == LoopKit.AbsoluteScheduleValue { map { LoopAlgorithm.AbsoluteScheduleValue(startDate: $0.startDate, endDate: $0.endDate, value: $0.value.doubleValue(for: .milligramsPerDeciliter)) } } - func algorithmMgdlRanges() -> LoopAlgorithm.GlucoseRangeTimeline where Element == LoopKit.AbsoluteScheduleValue> { + public func algorithmMgdlRanges() -> LoopAlgorithm.GlucoseRangeTimeline where Element == LoopKit.AbsoluteScheduleValue> { map { LoopAlgorithm.AbsoluteScheduleValue(startDate: $0.startDate, endDate: $0.endDate, value: $0.value.lowerBound.doubleValue(for: .milligramsPerDeciliter)...$0.value.upperBound.doubleValue(for: .milligramsPerDeciliter)) @@ -73,67 +74,56 @@ extension Array { // LoopAlgorithm's results, as LoopKit values for storage and charts. extension LoopAlgorithm.GlucoseEffect { - var loopKit: LoopKit.GlucoseEffect { + public var loopKit: LoopKit.GlucoseEffect { LoopKit.GlucoseEffect(startDate: startDate, quantity: LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: mgdl)) } } extension LoopAlgorithm.PredictedGlucoseValue { - var loopKit: LoopKit.PredictedGlucoseValue { + public var loopKit: LoopKit.PredictedGlucoseValue { LoopKit.PredictedGlucoseValue(startDate: startDate, quantity: LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: mgdl)) } } extension LoopAlgorithm.GlucoseEffectVelocity { - var loopKit: LoopKit.GlucoseEffectVelocity { + public var loopKit: LoopKit.GlucoseEffectVelocity { LoopKit.GlucoseEffectVelocity(startDate: startDate, endDate: endDate, quantity: LoopQuantity(unit: .milligramsPerDeciliterPerSecond, doubleValue: mgdlPerSecond)) } } extension LoopAlgorithm.CarbValue { - var loopKit: LoopKit.CarbValue { + public var loopKit: LoopKit.CarbValue { LoopKit.CarbValue(startDate: startDate, endDate: endDate, value: value) } } extension LoopAlgorithm.InsulinValue { - var loopKit: LoopKit.InsulinValue { + public var loopKit: LoopKit.InsulinValue { LoopKit.InsulinValue(startDate: startDate, value: value) } } -extension LoopAlgorithm.BasalRelativeDose { - var chartedDose: ChartedDose { - let type: ChartedDose.DoseType - switch self.type { - case .bolus: type = .bolus - case .basal: type = .basal - } - return ChartedDose(type: type, startDate: startDate, endDate: endDate, volume: volume, netBasalUnits: netBasalUnits) - } -} - extension LoopAlgorithm.GlucoseChange { - var loopKit: LoopKit.GlucoseChange { + public var loopKit: LoopKit.GlucoseChange { LoopKit.GlucoseChange(startDate: startDate, endDate: endDate, quantity: LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: mgdl)) } } extension LoopAlgorithm.SimpleGlucoseValue { - var loopKit: LoopKit.SimpleGlucoseValue { + public var loopKit: LoopKit.SimpleGlucoseValue { LoopKit.SimpleGlucoseValue(startDate: startDate, endDate: endDate, quantity: LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: mgdl)) } } extension LoopAlgorithm.TempBasalRecommendation { - var loopKit: LoopKit.TempBasalRecommendation { + public var loopKit: LoopKit.TempBasalRecommendation { LoopKit.TempBasalRecommendation(unitsPerHour: unitsPerHour, duration: duration) } } extension LoopAlgorithm.AutomaticDoseRecommendation.Direction { - var loopKit: LoopKit.AutomaticDoseRecommendation.Direction { + public var loopKit: LoopKit.AutomaticDoseRecommendation.Direction { switch self { case .decrease: .decrease case .neutral: .neutral @@ -143,13 +133,13 @@ extension LoopAlgorithm.AutomaticDoseRecommendation.Direction { } extension LoopAlgorithm.AutomaticDoseRecommendation { - var loopKit: LoopKit.AutomaticDoseRecommendation { + public var loopKit: LoopKit.AutomaticDoseRecommendation { LoopKit.AutomaticDoseRecommendation(basalAdjustment: basalAdjustment.loopKit, direction: direction.loopKit, bolusUnits: bolusUnits) } } extension LoopAlgorithm.BolusRecommendationNotice { - var loopKit: LoopKit.BolusRecommendationNotice { + public var loopKit: LoopKit.BolusRecommendationNotice { switch self { case .glucoseBelowSuspendThreshold(let minGlucose): return .glucoseBelowSuspendThreshold(minGlucose: minGlucose.loopKit) @@ -166,19 +156,19 @@ extension LoopAlgorithm.BolusRecommendationNotice { } extension LoopAlgorithm.ManualBolusRecommendation { - var loopKit: LoopKit.ManualBolusRecommendation { + public var loopKit: LoopKit.ManualBolusRecommendation { LoopKit.ManualBolusRecommendation(amount: amount, notice: notice?.loopKit) } } extension Array { - func loopKitSchedule() -> [LoopKit.AbsoluteScheduleValue] where Element == LoopAlgorithm.AbsoluteScheduleValue { + public func loopKitSchedule() -> [LoopKit.AbsoluteScheduleValue] where Element == LoopAlgorithm.AbsoluteScheduleValue { map { LoopKit.AbsoluteScheduleValue(startDate: $0.startDate, endDate: $0.endDate, value: $0.value) } } } extension LoopKit.TempBasalRecommendation { - var algorithm: LoopAlgorithm.TempBasalRecommendation { + public var algorithm: LoopAlgorithm.TempBasalRecommendation { LoopAlgorithm.TempBasalRecommendation(unitsPerHour: unitsPerHour, duration: duration) } } diff --git a/LoopCore/Models/BolusDosingDecision.swift b/LoopCore/Models/BolusDosingDecision.swift new file mode 100644 index 0000000000..efca0cc5d9 --- /dev/null +++ b/LoopCore/Models/BolusDosingDecision.swift @@ -0,0 +1,60 @@ +// +// BolusDosingDecision.swift +// Loop +// +// Created by Darin Krauss on 10/1/20. +// Copyright © 2020 LoopKit Authors. All rights reserved. +// + +import LoopKit + +public struct BolusDosingDecision { + public enum Reason: String { + case normalBolus + case simpleBolus + case watchBolus + } + + public var id: UUID + public var reason: Reason + public var scheduleOverride: TemporaryScheduleOverride? + public var historicalGlucose: [HistoricalGlucoseValue]? + public var originalCarbEntry: StoredCarbEntry? + public var carbEntry: StoredCarbEntry? + public var manualGlucoseSample: StoredGlucoseSample? + public var carbsOnBoard: CarbValue? + public var insulinOnBoard: InsulinValue? + public var glucoseTargetRangeSchedule: GlucoseRangeSchedule? + public var predictedGlucose: [PredictedGlucoseValue]? + public var manualBolusRecommendation: ManualBolusRecommendationWithDate? + public var manualBolusRequested: Double? + + public init(for reason: Reason) { + self.id = UUID() + self.reason = reason + } +} + +extension BolusDosingDecision { + /// The record of a bolus asked for on the watch: what the watch was shown, from one display run. + /// `glucoseTargetRangeSchedule` is the correction range in effect, as the device's presets work it out. + public static func watchBolus(displayState: AlgorithmDisplayState, recommendation: ManualBolusRecommendation?, + scheduleOverride: TemporaryScheduleOverride?, glucoseTargetRangeSchedule: GlucoseRangeSchedule?, + now: Date = Date()) -> BolusDosingDecision { + var decision = BolusDosingDecision(for: .watchBolus) + decision.carbsOnBoard = displayState.activeCarbs + decision.manualBolusRecommendation = recommendation.map { ManualBolusRecommendationWithDate(recommendation: $0, date: now) } + if let input = displayState.input, let latest = input.glucoseHistory.last { + let historicalGlucoseStartDate = now.addingTimeInterval(-LoopCoreConstants.dosingDecisionHistoricalGlucoseInterval) + let start = min(historicalGlucoseStartDate, latest.startDate) + decision.historicalGlucose = input.glucoseHistory.filterDateRange(start, nil) + .filter { $0.startDate >= historicalGlucoseStartDate } + .map { HistoricalGlucoseValue(startDate: $0.startDate, quantity: $0.quantity) } + } + decision.insulinOnBoard = displayState.activeInsulin + decision.predictedGlucose = displayState.output?.predictedGlucose.map(\.loopKit) + decision.scheduleOverride = scheduleOverride + decision.glucoseTargetRangeSchedule = glucoseTargetRangeSchedule + return decision + } +} diff --git a/Loop/Models/CarbEntryChange.swift b/LoopCore/Models/CarbEntryChange.swift similarity index 78% rename from Loop/Models/CarbEntryChange.swift rename to LoopCore/Models/CarbEntryChange.swift index 155ad34b53..2ea08eb0a2 100644 --- a/Loop/Models/CarbEntryChange.swift +++ b/LoopCore/Models/CarbEntryChange.swift @@ -9,46 +9,46 @@ import Foundation import LoopKit /// A carb entry about to be saved: a new entry, or new content for a stored one. -enum CarbEntryChange: Equatable { +public enum CarbEntryChange: Equatable { case add(NewCarbEntry) case edit(StoredCarbEntry, CarbEntryUpdate) - var originalEntry: StoredCarbEntry? { + public var originalEntry: StoredCarbEntry? { if case .edit(let original, _) = self { return original } return nil } - var quantity: LoopQuantity { + public var quantity: LoopQuantity { switch self { case .add(let entry): entry.quantity case .edit(_, let update): update.quantity } } - var startDate: Date { + public var startDate: Date { switch self { case .add(let entry): entry.startDate case .edit(_, let update): update.startDate } } - var foodType: String? { + public var foodType: String? { switch self { case .add(let entry): entry.foodType case .edit(_, let update): update.foodType } } - var absorptionTime: TimeInterval? { + public var absorptionTime: TimeInterval? { switch self { case .add(let entry): entry.absorptionTime case .edit(_, let update): update.absorptionTime } } - var asStoredCarbEntry: StoredCarbEntry { + public var asStoredCarbEntry: StoredCarbEntry { switch self { case .add(let entry): entry.asStoredCarbEntry case .edit(_, let update): update.asStoredCarbEntry diff --git a/LoopCore/Models/LoopConstants.swift b/LoopCore/Models/LoopConstants.swift new file mode 100644 index 0000000000..b1ce6249de --- /dev/null +++ b/LoopCore/Models/LoopConstants.swift @@ -0,0 +1,77 @@ +// +// LoopConstants.swift +// Loop +// +// Created by Pete Schwamb on 10/7/20. +// Copyright © 2020 LoopKit Authors. All rights reserved. +// + +import Foundation +import LoopKit +import UIKit + +public enum LoopConstants { + + // Input field bounds + + public static let maxCarbEntryQuantity = LoopQuantity(unit: .gram, doubleValue: 250) // cannot exceed this value + + public static let warningCarbEntryQuantity = LoopQuantity(unit: .gram, doubleValue: 99) // user is warned above this value + + public static let validManualGlucoseEntryRange = LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 10)...LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 600) + + public static let minCarbAbsorptionTime = TimeInterval(minutes: 30) + public static let maxCarbAbsorptionTime = TimeInterval(hours: 8) + + public static let maxCarbEntryPastTime = TimeInterval(hours: (-12)) + public static let maxCarbEntryFutureTime = TimeInterval(hours: 1) + + public static let maxOverrideDurationTime = TimeInterval(hours: 24) + + // MARK - Display settings + + public static let minimumChartWidthPerHour: CGFloat = 50 + + public static let statusChartMinimumHistoryDisplay: TimeInterval = .hours(1) + + /// Chart width the time window options are sized for: a portrait iPhone. + public static let statusChartReferenceWidth: CGFloat = 360 + + public static let statusChartMaximumWidthScale: CGFloat = 2 + + public static let chartMinimumWidthPerTimeLabel: CGFloat = 44 + + public static let glucoseChartDefaultDisplayBound = + LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 100)...LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 175) + + public static let glucoseChartDefaultDisplayRangeWide = + LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 60)...LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 200) + + public static let glucoseChartDefaultDisplayBoundClamped = + LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 80)...LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 240) + + + // Compile time configuration + + public static let retrospectiveCorrectionEnabled = true + + /// Loop completion aging category limits + public static let completionFreshLimit = TimeInterval(minutes: 6) + public static let completionAgingLimit = TimeInterval(minutes: 16) + public static let completionStaleLimit = TimeInterval(hours: 12) + + public static let batteryReplacementDetectionThreshold = 0.5 + + public static let defaultWatchCarbPickerValue = 15 // grams + + public static let defaultWatchBolusPickerValue = 1.0 // % + + /// Missed Meal warning constants + public static let missedMealWarningGlucoseRiseThreshold = 3.0 // mg/dL/m + public static let missedMealWarningGlucoseRecencyWindow = TimeInterval(minutes: 19) + + // Bolus calculator warning thresholds + public static let simpleBolusCalculatorMinGlucoseBolusRecommendation = LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 70) + public static let simpleBolusCalculatorMinGlucoseMealBolusRecommendation = LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 55) + public static let simpleBolusCalculatorGlucoseWarningLimit = LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 70) +} diff --git a/Loop/Models/LoopError.swift b/LoopCore/Models/LoopError.swift similarity index 55% rename from Loop/Models/LoopError.swift rename to LoopCore/Models/LoopError.swift index 0d1450318a..caf8fc5e93 100644 --- a/Loop/Models/LoopError.swift +++ b/LoopCore/Models/LoopError.swift @@ -9,7 +9,7 @@ import Foundation import LoopKit -enum ConfigurationErrorDetail: String, Codable { +public enum ConfigurationErrorDetail: String, Codable { case pumpManager case basalRateSchedule case carbRatioSchedule @@ -19,29 +19,29 @@ enum ConfigurationErrorDetail: String, Codable { case maximumBolus case suspendThreshold - func localized() -> String { + public func localized() -> String { switch self { case .pumpManager: - return NSLocalizedString("Pump Manager", comment: "Details for configuration error when pump manager is missing") + return LocalizedString("Pump Manager", comment: "Details for configuration error when pump manager is missing") case .basalRateSchedule: - return NSLocalizedString("Basal Rate Schedule", comment: "Details for configuration error when basal rate schedule is missing") + return LocalizedString("Basal Rate Schedule", comment: "Details for configuration error when basal rate schedule is missing") case .carbRatioSchedule: - return NSLocalizedString("Carb Ratio Schedule", comment: "Details for configuration error when carb ratio schedule is missing") + return LocalizedString("Carb Ratio Schedule", comment: "Details for configuration error when carb ratio schedule is missing") case .glucoseTargetRangeSchedule: - return NSLocalizedString("Glucose Target Range Schedule", comment: "Details for configuration error when glucose target range schedule is missing") + return LocalizedString("Glucose Target Range Schedule", comment: "Details for configuration error when glucose target range schedule is missing") case .insulinSensitivitySchedule: - return NSLocalizedString("Insulin Sensitivity Schedule", comment: "Details for configuration error when insulin sensitivity schedule is missing") + return LocalizedString("Insulin Sensitivity Schedule", comment: "Details for configuration error when insulin sensitivity schedule is missing") case .maximumBasalRatePerHour: - return NSLocalizedString("Maximum Basal Rate Per Hour", comment: "Details for configuration error when maximum basal rate per hour is missing") + return LocalizedString("Maximum Basal Rate Per Hour", comment: "Details for configuration error when maximum basal rate per hour is missing") case .maximumBolus: - return NSLocalizedString("Maximum Bolus", comment: "Details for configuration error when maximum bolus is missing") + return LocalizedString("Maximum Bolus", comment: "Details for configuration error when maximum bolus is missing") case .suspendThreshold: - return NSLocalizedString("Suspend Threshold", comment: "Details for configuration error when suspend threshold is missing") + return LocalizedString("Suspend Threshold", comment: "Details for configuration error when suspend threshold is missing") } } } -enum MissingDataErrorDetail: String, Codable { +public enum MissingDataErrorDetail: String, Codable { case glucose case momentumEffect case carbEffect @@ -49,27 +49,27 @@ enum MissingDataErrorDetail: String, Codable { case activeInsulin case insulinEffectIncludingPendingInsulin - var localizedDetail: String { + public var localizedDetail: String { switch self { case .glucose: - return NSLocalizedString("Glucose data not available", comment: "Description of error when glucose data is missing") + return LocalizedString("Glucose data not available", comment: "Description of error when glucose data is missing") case .momentumEffect: - return NSLocalizedString("Momentum effects", comment: "Details for missing data error when momentum effects are missing") + return LocalizedString("Momentum effects", comment: "Details for missing data error when momentum effects are missing") case .carbEffect: - return NSLocalizedString("Carb effects", comment: "Details for missing data error when carb effects are missing") + return LocalizedString("Carb effects", comment: "Details for missing data error when carb effects are missing") case .insulinEffect: - return NSLocalizedString("Insulin effects", comment: "Details for missing data error when insulin effects are missing") + return LocalizedString("Insulin effects", comment: "Details for missing data error when insulin effects are missing") case .activeInsulin: - return NSLocalizedString("Active Insulin", comment: "Details for missing data error when active insulin amount is missing") + return LocalizedString("Active Insulin", comment: "Details for missing data error when active insulin amount is missing") case .insulinEffectIncludingPendingInsulin: - return NSLocalizedString("Insulin effects", comment: "Details for missing data error when insulin effects including pending insulin are missing") + return LocalizedString("Insulin effects", comment: "Details for missing data error when insulin effects including pending insulin are missing") } } - var localizedRecovery: String? { + public var localizedRecovery: String? { switch self { case .glucose: - return NSLocalizedString("Check your CGM data source", comment: "Recovery suggestion when glucose data is missing") + return LocalizedString("Check your CGM data source", comment: "Recovery suggestion when glucose data is missing") case .momentumEffect: return nil case .carbEffect: @@ -80,7 +80,7 @@ enum MissingDataErrorDetail: String, Codable { } } -enum LoopError: Error { +public enum LoopError: Error { // Missing or unexpected configuration values case configurationError(ConfigurationErrorDetail) @@ -121,7 +121,7 @@ enum LoopError: Error { case unknownError(Error) } -extension LoopError { +public extension LoopError { var issue: StoredDosingDecision.Issue { return StoredDosingDecision.Issue(id: issueId, details: issueDetails) } @@ -201,35 +201,35 @@ extension LoopError: LocalizedError { switch self { case .configurationError(let details): - return String(format: NSLocalizedString("Configuration Error: %1$@", comment: "The error message displayed for configuration errors. (1: configuration error details)"), details.localized()) + return String(format: LocalizedString("Configuration Error: %1$@", comment: "The error message displayed for configuration errors. (1: configuration error details)"), details.localized()) case .connectionError: - return NSLocalizedString("No connected devices, or failure during device connection", comment: "The error message displayed for device connection errors.") + return LocalizedString("No connected devices, or failure during device connection", comment: "The error message displayed for device connection errors.") case .missingDataError(let details): - return String(format: NSLocalizedString("Missing data: %1$@", comment: "The error message for missing data. (1: missing data details)"), details.localizedDetail) + return String(format: LocalizedString("Missing data: %1$@", comment: "The error message for missing data. (1: missing data details)"), details.localizedDetail) case .glucoseTooOld(let date): let minutes = formatter.string(from: -date.timeIntervalSinceNow) ?? "" - return String(format: NSLocalizedString("Glucose data is %1$@ old", comment: "The error message when glucose data is too old to be used. (1: glucose data age in minutes)"), minutes) + return String(format: LocalizedString("Glucose data is %1$@ old", comment: "The error message when glucose data is too old to be used. (1: glucose data age in minutes)"), minutes) case .invalidFutureGlucose(let date): let minutes = formatter.string(from: -date.timeIntervalSinceNow) ?? "" - return String(format: NSLocalizedString("Invalid glucose reading with a timestamp that is %1$@ in the future", comment: "The error message when glucose data is in the future. (1: glucose data time in future in minutes)"), minutes) + return String(format: LocalizedString("Invalid glucose reading with a timestamp that is %1$@ in the future", comment: "The error message when glucose data is in the future. (1: glucose data time in future in minutes)"), minutes) case .pumpDataTooOld(let date): let minutes = formatter.string(from: -date.timeIntervalSinceNow) ?? "" - return String(format: NSLocalizedString("Pump data is %1$@ old", comment: "The error message when pump data is too old to be used. (1: pump data age in minutes)"), minutes) + return String(format: LocalizedString("Pump data is %1$@ old", comment: "The error message when pump data is too old to be used. (1: pump data age in minutes)"), minutes) case .recommendationExpired(let date): let minutes = formatter.string(from: -date.timeIntervalSinceNow) ?? "" - return String(format: NSLocalizedString("Recommendation expired: %1$@ old", comment: "The error message when a recommendation has expired. (1: age of recommendation in minutes)"), minutes) + return String(format: LocalizedString("Recommendation expired: %1$@ old", comment: "The error message when a recommendation has expired. (1: age of recommendation in minutes)"), minutes) case .pumpInoperable: - return NSLocalizedString("Pump Inoperable. Automatic dosing is disabled.", comment: "The error message displayed for LoopError.pumpInoperable errors.") + return LocalizedString("Pump Inoperable. Automatic dosing is disabled.", comment: "The error message displayed for LoopError.pumpInoperable errors.") case .pumpSuspended: - return NSLocalizedString("Pump Suspended. Automatic dosing is disabled.", comment: "The error message displayed for LoopError.pumpSuspended errors.") + return LocalizedString("Pump Suspended. Automatic dosing is disabled.", comment: "The error message displayed for LoopError.pumpSuspended errors.") case .manualTempBasalRunning: - return NSLocalizedString("Manual temporary basal in progress. Automatic dosing is disabled.", comment: "The error message displayed for LoopError.manualTempBasalRunning errors.") + return LocalizedString("Manual temporary basal in progress. Automatic dosing is disabled.", comment: "The error message displayed for LoopError.manualTempBasalRunning errors.") case .pumpManagerError(let pumpManagerError): - return String(format: NSLocalizedString("Pump Manager Error: %1$@", comment: "The error message displayed for pump manager errors. (1: pump manager error)"), pumpManagerError.errorDescription!) + return String(format: LocalizedString("Pump Manager Error: %1$@", comment: "The error message displayed for pump manager errors. (1: pump manager error)"), pumpManagerError.errorDescription!) case .loopInProgress: - return NSLocalizedString("Loop is already looping.", comment: "The error message displayed for LoopError.loopInProgress errors.") + return LocalizedString("Loop is already looping.", comment: "The error message displayed for LoopError.loopInProgress errors.") case .unknownError(let error): - return String(format: NSLocalizedString("Unknown Error: %1$@", comment: "The error message displayed for unknown LoopError errors. (1: unknown error)"), error.localizedDescription) + return String(format: LocalizedString("Unknown Error: %1$@", comment: "The error message displayed for unknown LoopError errors. (1: unknown error)"), error.localizedDescription) } } } diff --git a/Loop/Models/LoopWarning.swift b/LoopCore/Models/LoopWarning.swift similarity index 93% rename from Loop/Models/LoopWarning.swift rename to LoopCore/Models/LoopWarning.swift index 45439b3c55..36ba4abe6b 100644 --- a/Loop/Models/LoopWarning.swift +++ b/LoopCore/Models/LoopWarning.swift @@ -9,7 +9,7 @@ import Foundation import LoopKit -enum FetchDataWarningDetail { +public enum FetchDataWarningDetail { case glucoseSamples(error: Error) case glucoseMomentumEffect(error: Error) case insulinEffect(error: Error) @@ -21,7 +21,7 @@ enum FetchDataWarningDetail { case retrospectiveGlucoseEffect(error: Error) } -extension FetchDataWarningDetail { +public extension FetchDataWarningDetail { var issueId: String { switch self { case .glucoseSamples: @@ -63,12 +63,12 @@ extension FetchDataWarningDetail { } } -enum LoopWarning { +public enum LoopWarning { case fetchDataWarning(FetchDataWarningDetail) case bolusInProgress } -extension LoopWarning { +public extension LoopWarning { var issue: StoredDosingDecision.Issue { return StoredDosingDecision.Issue(id: issueId, details: issueDetails) } @@ -94,6 +94,6 @@ extension LoopWarning { } } -extension Locked where T == [LoopWarning] { +public extension Locked where T == [LoopWarning] { func append(_ loopWarning: LoopWarning) { mutate { $0.append(loopWarning) } } } diff --git a/Loop/Models/SimpleInsulinDose.swift b/LoopCore/Models/SimpleInsulinDose.swift similarity index 74% rename from Loop/Models/SimpleInsulinDose.swift rename to LoopCore/Models/SimpleInsulinDose.swift index c3963b47ce..4a5c2afef1 100644 --- a/Loop/Models/SimpleInsulinDose.swift +++ b/LoopCore/Models/SimpleInsulinDose.swift @@ -13,13 +13,22 @@ 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 automatic: Bool - var startDate: Date - var endDate: Date - var volume: Double - var insulinModel: LoopAlgorithm.InsulinModel +public struct SimpleInsulinDose: InsulinDose { + public var deliveryType: InsulinDeliveryType + public var automatic: Bool + public var startDate: Date + public var endDate: Date + public var volume: Double + public var insulinModel: LoopAlgorithm.InsulinModel + + public init(deliveryType: InsulinDeliveryType, automatic: Bool, startDate: Date, endDate: Date, volume: Double, insulinModel: LoopAlgorithm.InsulinModel) { + self.deliveryType = deliveryType + self.automatic = automatic + self.startDate = startDate + self.endDate = endDate + self.volume = volume + self.insulinModel = insulinModel + } } extension DoseEntry { @@ -36,7 +45,7 @@ extension DoseEntry { return deliveredUnits ?? programmedUnits } - func simpleDose(with model: LoopKit.InsulinModel) -> SimpleInsulinDose { + public func simpleDose(with model: LoopKit.InsulinModel) -> SimpleInsulinDose { SimpleInsulinDose( deliveryType: deliveryType, automatic: automatic ?? false, @@ -48,7 +57,7 @@ extension DoseEntry { } } -extension Array where Element == SimpleInsulinDose { +public extension Array where Element == SimpleInsulinDose { func trimmed(to end: Date? = nil) -> [SimpleInsulinDose] { return self.compactMap { (dose) -> SimpleInsulinDose? in if let end, dose.startDate > end { diff --git a/LoopCore/Models/StoredDataAlgorithmInput.swift b/LoopCore/Models/StoredDataAlgorithmInput.swift new file mode 100644 index 0000000000..bce19a0808 --- /dev/null +++ b/LoopCore/Models/StoredDataAlgorithmInput.swift @@ -0,0 +1,63 @@ +// +// StoredDataAlgorithmInput.swift +// Loop +// +// Created by Pete Schwamb on 2/23/24. +// Copyright © 2024 LoopKit Authors. All rights reserved. +// + +import Foundation +import LoopKit +import LoopAlgorithm + +public struct StoredDataAlgorithmInput: AlgorithmInput { + public typealias CarbType = StoredCarbEntry + + public typealias GlucoseType = StoredGlucoseSample + + public typealias InsulinDoseType = SimpleInsulinDose + + public var glucoseHistory: [StoredGlucoseSample] + + public var doses: [SimpleInsulinDose] + + public var carbEntries: [StoredCarbEntry] + + public var predictionStart: Date + + public var basal: [LoopAlgorithm.AbsoluteScheduleValue] + + public var sensitivityMgdlPerUnit: [LoopAlgorithm.AbsoluteScheduleValue] + + public var carbRatio: [LoopAlgorithm.AbsoluteScheduleValue] + + public var targetMgdl: LoopAlgorithm.GlucoseRangeTimeline + + public var suspendThresholdMgdl: Double? + + public var maxBolus: Double + + public var maxBasalRate: Double + + public var useIntegralRetrospectiveCorrection: Bool + + public var includePositiveVelocityAndRC: Bool + + public var carbAbsorptionModel: CarbAbsorptionModel + + public var recommendationInsulinModel: LoopAlgorithm.InsulinModel + + public var recommendationType: DoseRecommendationType + + public var automaticBolusApplicationFactor: Double? + + public let useMidAbsorptionISF: Bool = true + + public var maxActiveInsulinMultiplier: Double? = nil + + public var gradualTransitionsThreshold: Double? = nil + + public var experiments: Set = [] + + public var volatilityBaseline: Double? = nil +} diff --git a/LoopTests/Managers/MealDetectionManagerTests.swift b/LoopTests/Managers/MealDetectionManagerTests.swift index ab07e9cb87..35cc17d9a4 100644 --- a/LoopTests/Managers/MealDetectionManagerTests.swift +++ b/LoopTests/Managers/MealDetectionManagerTests.swift @@ -8,7 +8,7 @@ import XCTest import LoopAlgorithm -import LoopCore +@testable import LoopCore import LoopKit @testable import Loop diff --git a/LoopTests/Mock Stores/MockCarbStore.swift b/LoopTests/Mock Stores/MockCarbStore.swift index 05e3dd81b1..8dda5d05e1 100644 --- a/LoopTests/Mock Stores/MockCarbStore.swift +++ b/LoopTests/Mock Stores/MockCarbStore.swift @@ -7,7 +7,7 @@ // import LoopKit -import LoopCore +@testable import LoopCore @testable import Loop class MockCarbStore: CarbStoreProtocol { diff --git a/LoopTests/Mock Stores/MockDoseStore.swift b/LoopTests/Mock Stores/MockDoseStore.swift index 51331229e6..f7303e865d 100644 --- a/LoopTests/Mock Stores/MockDoseStore.swift +++ b/LoopTests/Mock Stores/MockDoseStore.swift @@ -9,6 +9,7 @@ import HealthKit import LoopKit @testable import Loop +@testable import LoopCore class MockDoseStore: DoseStoreProtocol { func getNormalizedDoseEntries(start: Date, end: Date?) async throws -> [LoopKit.DoseEntry] { diff --git a/LoopTests/Mock Stores/MockDosingDecisionStore.swift b/LoopTests/Mock Stores/MockDosingDecisionStore.swift index a53d30afd4..0d68ca5400 100644 --- a/LoopTests/Mock Stores/MockDosingDecisionStore.swift +++ b/LoopTests/Mock Stores/MockDosingDecisionStore.swift @@ -9,6 +9,7 @@ import LoopKit import XCTest @testable import Loop +@testable import LoopCore class MockDosingDecisionStore: DosingDecisionStoreProtocol { var delegate: LoopKit.DosingDecisionStoreDelegate? diff --git a/LoopTests/Mock Stores/MockGlucoseStore.swift b/LoopTests/Mock Stores/MockGlucoseStore.swift index 5ec9452116..90ff0d44b0 100644 --- a/LoopTests/Mock Stores/MockGlucoseStore.swift +++ b/LoopTests/Mock Stores/MockGlucoseStore.swift @@ -8,6 +8,7 @@ import LoopKit @testable import Loop +@testable import LoopCore class MockGlucoseStore: GlucoseStoreProtocol { diff --git a/LoopTests/Mocks/LoopControlMock.swift b/LoopTests/Mocks/LoopControlMock.swift index 3c03d8f9ee..1bac367447 100644 --- a/LoopTests/Mocks/LoopControlMock.swift +++ b/LoopTests/Mocks/LoopControlMock.swift @@ -10,6 +10,7 @@ import XCTest import Foundation import LoopKit @testable import Loop +@testable import LoopCore class LoopControlMock: LoopControl { diff --git a/LoopTests/Mocks/MockDeliveryDelegate.swift b/LoopTests/Mocks/MockDeliveryDelegate.swift index 1f37b4bbd1..72fac055cc 100644 --- a/LoopTests/Mocks/MockDeliveryDelegate.swift +++ b/LoopTests/Mocks/MockDeliveryDelegate.swift @@ -9,6 +9,7 @@ import Foundation import LoopKit @testable import Loop +@testable import LoopCore class MockDeliveryDelegate: DeliveryDelegate { var isSuspended: Bool = false diff --git a/LoopTests/Mocks/MockSettingsProvider.swift b/LoopTests/Mocks/MockSettingsProvider.swift index 975a63dff1..cb1833af28 100644 --- a/LoopTests/Mocks/MockSettingsProvider.swift +++ b/LoopTests/Mocks/MockSettingsProvider.swift @@ -9,6 +9,7 @@ import Foundation import LoopKit @testable import Loop +@testable import LoopCore @Observable class MockSettingsProvider: SettingsProvider { diff --git a/LoopTests/Models/InsulinDeliveryHistoryTests.swift b/LoopTests/Models/InsulinDeliveryHistoryTests.swift index ddd22f0141..4059d7bb33 100644 --- a/LoopTests/Models/InsulinDeliveryHistoryTests.swift +++ b/LoopTests/Models/InsulinDeliveryHistoryTests.swift @@ -9,6 +9,7 @@ import XCTest import LoopAlgorithm import LoopKit @testable import Loop +@testable import LoopCore final class InsulinDeliveryHistoryTests: XCTestCase { private var calendar: Calendar = { diff --git a/LoopTests/Models/SetupComparisonTests.swift b/LoopTests/Models/SetupComparisonTests.swift index 30ccdb6462..562defc4fc 100644 --- a/LoopTests/Models/SetupComparisonTests.swift +++ b/LoopTests/Models/SetupComparisonTests.swift @@ -7,6 +7,7 @@ import XCTest import LoopAlgorithm +import LoopCore import LoopKit @testable import Loop diff --git a/LoopTests/ViewModels/BolusEntryViewModelTests.swift b/LoopTests/ViewModels/BolusEntryViewModelTests.swift index 0287fbedbe..1446bcc24b 100644 --- a/LoopTests/ViewModels/BolusEntryViewModelTests.swift +++ b/LoopTests/ViewModels/BolusEntryViewModelTests.swift @@ -8,7 +8,7 @@ import HealthKit import LoopAlgorithm -import LoopCore +@testable import LoopCore import LoopKit import LoopKitUI import SwiftUI diff --git a/LoopTests/ViewModels/ManualEntryDoseViewModelTests.swift b/LoopTests/ViewModels/ManualEntryDoseViewModelTests.swift index 15a1cc4414..67dc246d89 100644 --- a/LoopTests/ViewModels/ManualEntryDoseViewModelTests.swift +++ b/LoopTests/ViewModels/ManualEntryDoseViewModelTests.swift @@ -6,7 +6,7 @@ // Copyright © 2021 LoopKit Authors. All rights reserved. // -import LoopCore +@testable import LoopCore import LoopKit import XCTest