diff --git a/Common/Models/PumpManager.swift b/Common/Models/PumpManager.swift index 3c2486ef68..5ec574366c 100644 --- a/Common/Models/PumpManager.swift +++ b/Common/Models/PumpManager.swift @@ -12,13 +12,13 @@ import MockKit import MockKitUI let staticPumpManagersByIdentifier: [String: PumpManagerUI.Type] = [ - MockPumpManager.managerIdentifier : MockPumpManager.self + MockPumpManager.pluginIdentifier : MockPumpManager.self ] var availableStaticPumpManagers: [PumpManagerDescriptor] { if FeatureFlags.allowSimulators { return [ - PumpManagerDescriptor(identifier: MockPumpManager.managerIdentifier, localizedTitle: MockPumpManager.localizedTitle) + PumpManagerDescriptor(identifier: MockPumpManager.pluginIdentifier, localizedTitle: MockPumpManager.localizedTitle) ] } else { return [] @@ -31,7 +31,7 @@ extension PumpManager { var rawValue: RawValue { return [ - "managerIdentifier": self.managerIdentifier, + "managerIdentifier": self.pluginIdentifier, "state": self.rawState ] } diff --git a/Common/hi.lproj/Intents.strings b/Common/hi.lproj/Intents.strings new file mode 100644 index 0000000000..853af215c0 --- /dev/null +++ b/Common/hi.lproj/Intents.strings @@ -0,0 +1,24 @@ +"80eo5o" = "Add Carb Entry"; + +"9KhaIS" = "I've set the preset"; + +"I4OZy8" = "Enable Override Preset"; + +"OcNxIj" = "Add Carb Entry"; + +"XNNmtH" = "Enable preset in Loop"; + +"ZZ3mtM" = "Enable an override preset in Loop"; + +"b085BW" = "I wasn't able to set the preset."; + +"lYMuWV" = "Override Name"; + +"nDKAmn" = "What's the name of the override you'd like to set?"; + +"oLQSsJ" = "Enable '${overrideName}' Override Preset"; + +"yBzwCL" = "Override Selection"; + +"yc02Yq" = "Add a carb entry to Loop"; + diff --git a/Loop Status Extension/it.lproj/Localizable.strings b/Loop Status Extension/it.lproj/Localizable.strings index 871ef62d8c..9404086e35 100644 --- a/Loop Status Extension/it.lproj/Localizable.strings +++ b/Loop Status Extension/it.lproj/Localizable.strings @@ -14,7 +14,7 @@ "%1$@ v%2$@" = "%1$@ contro %2$@"; /* Widget label title describing the active carbs */ -"Active Carbs" = "Carb Attivi"; +"Active Carbs" = "Carboidrati Attivi"; /* Widget label title describing the active insulin */ "Active Insulin" = "Insulina attiva"; diff --git a/Loop Widget Extension/Helpers/ContentMargin.swift b/Loop Widget Extension/Helpers/ContentMargin.swift new file mode 100644 index 0000000000..92a2d41786 --- /dev/null +++ b/Loop Widget Extension/Helpers/ContentMargin.swift @@ -0,0 +1,20 @@ +// +// ContentMargin.swift +// Loop Widget Extension +// +// Created by Cameron Ingham on 9/29/23. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import SwiftUI +import WidgetKit + +extension WidgetConfiguration { + func contentMarginsDisabledIfAvailable() -> some WidgetConfiguration { + if #available(iOSApplicationExtension 17.0, *) { + return self.contentMarginsDisabled() + } else { + return self + } + } +} diff --git a/Loop Widget Extension/Helpers/WidgetBackground.swift b/Loop Widget Extension/Helpers/WidgetBackground.swift index 9883f4917a..f5202f092c 100644 --- a/Loop Widget Extension/Helpers/WidgetBackground.swift +++ b/Loop Widget Extension/Helpers/WidgetBackground.swift @@ -11,6 +11,12 @@ import SwiftUI extension View { @ViewBuilder func widgetBackground() -> some View { - self.background { Color("WidgetBackground") } + if #available(iOSApplicationExtension 17.0, *) { + self.containerBackground(for: .widget) { + Color("WidgetBackground") + } + } else { + self.background { Color("WidgetBackground") } + } } } diff --git a/Loop Widget Extension/Widgets/SystemStatusWidget.swift b/Loop Widget Extension/Widgets/SystemStatusWidget.swift index 8546409b5c..a64096d2ad 100644 --- a/Loop Widget Extension/Widgets/SystemStatusWidget.swift +++ b/Loop Widget Extension/Widgets/SystemStatusWidget.swift @@ -76,5 +76,6 @@ struct SystemStatusWidget: Widget { .configurationDisplayName("Loop Status Widget") .description("See your current blood glucose and insulin delivery.") .supportedFamilies([.systemSmall, .systemMedium]) + .contentMarginsDisabledIfAvailable() } } diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index aaa0a470ac..1181951609 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -24,7 +24,7 @@ 149A28E42A8A63A700052EDF /* FavoriteFoodDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149A28E32A8A63A700052EDF /* FavoriteFoodDetailView.swift */; }; 14B1735E28AED9EC006CCD7C /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14B1735D28AED9EC006CCD7C /* WidgetKit.framework */; }; 14B1736028AED9EC006CCD7C /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14B1735F28AED9EC006CCD7C /* SwiftUI.framework */; }; - 14B1736928AED9EE006CCD7C /* SmallStatusWidgetExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 14B1735C28AED9EC006CCD7C /* SmallStatusWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 14B1736928AED9EE006CCD7C /* Loop Widget Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 14B1735C28AED9EC006CCD7C /* Loop Widget Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 14B1737228AEDBF6006CCD7C /* BasalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B1736E28AEDBF6006CCD7C /* BasalView.swift */; }; 14B1737328AEDBF6006CCD7C /* SystemStatusWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B1736F28AEDBF6006CCD7C /* SystemStatusWidget.swift */; }; 14B1737428AEDBF6006CCD7C /* GlucoseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B1737028AEDBF6006CCD7C /* GlucoseView.swift */; }; @@ -253,6 +253,7 @@ 84AA81E32A4A36FB000B658B /* SystemActionLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AA81E22A4A36FB000B658B /* SystemActionLink.swift */; }; 84AA81E52A4A3981000B658B /* DeeplinkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AA81E42A4A3981000B658B /* DeeplinkManager.swift */; }; 84AA81E72A4A4DEF000B658B /* PumpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AA81E62A4A4DEF000B658B /* PumpView.swift */; }; + 84D2879F2AC756C8007ED283 /* ContentMargin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D2879E2AC756C8007ED283 /* ContentMargin.swift */; }; 891B508524342BE1005DA578 /* CarbAndBolusFlowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 891B508424342BE1005DA578 /* CarbAndBolusFlowViewModel.swift */; }; 892A5D59222F0A27008961AB /* Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 892A5D58222F0A27008961AB /* Debug.swift */; }; 892A5D692230C41D008961AB /* RangeReplaceableCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 892A5D682230C41D008961AB /* RangeReplaceableCollection.swift */; }; @@ -374,6 +375,7 @@ B42D124328D371C400E43D22 /* AlertMuter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B42D124228D371C400E43D22 /* AlertMuter.swift */; }; B43CF07E29434EC4008A520B /* HowMuteAlertWorkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B43CF07D29434EC4008A520B /* HowMuteAlertWorkView.swift */; }; B43DA44124D9C12100CAFF4E /* DismissibleHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B43DA44024D9C12100CAFF4E /* DismissibleHostingController.swift */; }; + B470F5842AB22B5100049695 /* StatefulPluggable.swift in Sources */ = {isa = PBXBuildFile; fileRef = B470F5832AB22B5100049695 /* StatefulPluggable.swift */; }; B48B0BAC24900093009A48DE /* PumpStatusHUDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B48B0BAB24900093009A48DE /* PumpStatusHUDView.swift */; }; B490A03F24D0550F00F509FA /* GlucoseRangeCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B490A03E24D0550F00F509FA /* GlucoseRangeCategory.swift */; }; B490A04124D0559D00F509FA /* DeviceLifecycleProgressState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B490A04024D0559D00F509FA /* DeviceLifecycleProgressState.swift */; }; @@ -387,6 +389,7 @@ B4CAD8792549D2540057946B /* LoopCompletionFreshnessTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4CAD8782549D2540057946B /* LoopCompletionFreshnessTests.swift */; }; B4D4534128E5CA7900F1A8D9 /* AlertMuterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D4534028E5CA7900F1A8D9 /* AlertMuterTests.swift */; }; B4D620D424D9EDB900043B3C /* GuidanceColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D620D324D9EDB900043B3C /* GuidanceColors.swift */; }; + B4D904412AA8989100CBD826 /* StatefulPluginManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D904402AA8989100CBD826 /* StatefulPluginManager.swift */; }; B4E202302661063E009421B5 /* AutomaticDosingStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4E2022F2661063E009421B5 /* AutomaticDosingStatus.swift */; }; B4E96D4B248A6B6E002DABAD /* DeviceStatusHUDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4E96D4A248A6B6E002DABAD /* DeviceStatusHUDView.swift */; }; B4E96D4F248A6E20002DABAD /* CGMStatusHUDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4E96D4E248A6E20002DABAD /* CGMStatusHUDView.swift */; }; @@ -414,9 +417,6 @@ C1201E2C23ECDBD0002DA84A /* WatchContextRequestUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1201E2B23ECDBD0002DA84A /* WatchContextRequestUserInfo.swift */; }; C1201E2D23ECDF3D002DA84A /* WatchContextRequestUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1201E2B23ECDBD0002DA84A /* WatchContextRequestUserInfo.swift */; }; C13072BA2A76AF31009A7C58 /* live_capture_predicted_glucose.json in Resources */ = {isa = PBXBuildFile; fileRef = C13072B92A76AF31009A7C58 /* live_capture_predicted_glucose.json */; }; - C13072BE2A76AF97009A7C58 /* live_capture_doses.json in Resources */ = {isa = PBXBuildFile; fileRef = C13072BD2A76AF97009A7C58 /* live_capture_doses.json */; }; - C13072C02A76B041009A7C58 /* live_capture_carb_entries.json in Resources */ = {isa = PBXBuildFile; fileRef = C13072BF2A76B041009A7C58 /* live_capture_carb_entries.json */; }; - C13072C42A76B0B1009A7C58 /* live_capture_historic_glucose.json in Resources */ = {isa = PBXBuildFile; fileRef = C13072C32A76B0B1009A7C58 /* live_capture_historic_glucose.json */; }; C13255D6223E7BE2008AF50C /* BolusProgressTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = C1F8B1DB223862D500DD66CF /* BolusProgressTableViewCell.xib */; }; C13DA2B024F6C7690098BB29 /* UIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13DA2AF24F6C7690098BB29 /* UIViewController.swift */; }; C148CEE724FD91BD00711B3B /* DeliveryUncertaintyAlertManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C148CEE624FD91BD00711B3B /* DeliveryUncertaintyAlertManager.swift */; }; @@ -434,6 +434,7 @@ C16B983E26B4893300256B05 /* DoseEnactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C16B983D26B4893300256B05 /* DoseEnactor.swift */; }; C16B984026B4898800256B05 /* DoseEnactorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C16B983F26B4898800256B05 /* DoseEnactorTests.swift */; }; C16DA84222E8E112008624C2 /* PluginManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C16DA84122E8E112008624C2 /* PluginManager.swift */; }; + C16FC0B02A99392F0025E239 /* live_capture_input.json in Resources */ = {isa = PBXBuildFile; fileRef = C16FC0AF2A99392F0025E239 /* live_capture_input.json */; }; C1735B1E2A0809830082BB8A /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = C1735B1D2A0809830082BB8A /* ZIPFoundation */; }; C1742332259BEADC00399C9D /* ManualEntryDoseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1742331259BEADC00399C9D /* ManualEntryDoseView.swift */; }; C174233C259BEB0F00399C9D /* ManualEntryDoseViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C174233B259BEB0F00399C9D /* ManualEntryDoseViewModel.swift */; }; @@ -470,6 +471,7 @@ C1D0B6302986D4D90098D215 /* LocalizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D0B62F2986D4D90098D215 /* LocalizedString.swift */; }; C1D0B6312986D4D90098D215 /* LocalizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D0B62F2986D4D90098D215 /* LocalizedString.swift */; }; C1D289B522F90A52003FFBD9 /* BasalDeliveryState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D289B422F90A52003FFBD9 /* BasalDeliveryState.swift */; }; + C1D476B42A8ED179002C1C87 /* LoopAlgorithmTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D476B32A8ED179002C1C87 /* LoopAlgorithmTests.swift */; }; C1D6EEA02A06C7270047DE5C /* MKRingProgressView in Frameworks */ = {isa = PBXBuildFile; productRef = C1D6EE9F2A06C7270047DE5C /* MKRingProgressView */; }; C1DE5D23251BFC4D00439E49 /* SimpleBolusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1DE5D22251BFC4D00439E49 /* SimpleBolusView.swift */; }; C1E2773E224177C000354103 /* ClockKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C1E2773D224177C000354103 /* ClockKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; @@ -479,7 +481,7 @@ C1EF747228D6A44A00C8C083 /* CrashRecoveryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1EF747128D6A44A00C8C083 /* CrashRecoveryManager.swift */; }; C1F00C60285A802A006302C5 /* SwiftCharts in Frameworks */ = {isa = PBXBuildFile; productRef = C1F00C5F285A802A006302C5 /* SwiftCharts */; }; C1F00C78285A8256006302C5 /* SwiftCharts in Embed Frameworks */ = {isa = PBXBuildFile; productRef = C1F00C5F285A802A006302C5 /* SwiftCharts */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - C1F2075C26D6F9B0007AB7EB /* ProfileExpirationAlerter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F2075B26D6F9B0007AB7EB /* ProfileExpirationAlerter.swift */; }; + C1F2075C26D6F9B0007AB7EB /* AppExpirationAlerter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F2075B26D6F9B0007AB7EB /* AppExpirationAlerter.swift */; }; C1F7822627CC056900C0919A /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F7822527CC056900C0919A /* SettingsManager.swift */; }; C1F8B243223E73FD00DD66CF /* BolusProgressTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F8B1D122375E4200DD66CF /* BolusProgressTableViewCell.swift */; }; C1FB428C217806A400FAB378 /* StateColorPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FB428B217806A300FAB378 /* StateColorPalette.swift */; }; @@ -720,7 +722,7 @@ dstPath = ""; dstSubfolderSpec = 13; files = ( - 14B1736928AED9EE006CCD7C /* SmallStatusWidgetExtension.appex in Embed App Extensions */, + 14B1736928AED9EE006CCD7C /* Loop Widget Extension.appex in Embed App Extensions */, E9B07F94253BBA6500BAD8F8 /* Loop Intent Extension.appex in Embed App Extensions */, 4F70C1E81DE8DCA7006380B7 /* Loop Status Extension.appex in Embed App Extensions */, ); @@ -752,7 +754,7 @@ 149A28BA2A853E5100052EDF /* CarbEntryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbEntryViewModel.swift; sourceTree = ""; }; 149A28BC2A853E6C00052EDF /* CarbEntryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbEntryView.swift; sourceTree = ""; }; 149A28E32A8A63A700052EDF /* FavoriteFoodDetailView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FavoriteFoodDetailView.swift; sourceTree = ""; }; - 14B1735C28AED9EC006CCD7C /* SmallStatusWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; name = SmallStatusWidgetExtension.appex; path = "Loop Widget Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; + 14B1735C28AED9EC006CCD7C /* Loop Widget Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Loop Widget Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 14B1735D28AED9EC006CCD7C /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; 14B1735F28AED9EC006CCD7C /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; 14B1736628AED9EE006CCD7C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -788,6 +790,7 @@ 1DC63E7325351BDF004605DA /* TrueTime.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = TrueTime.framework; path = Carthage/Build/iOS/TrueTime.framework; sourceTree = ""; }; 1DE09BA824A3E23F009EE9F9 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 1DFE9E162447B6270082C280 /* UserNotificationAlertSchedulerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationAlertSchedulerTests.swift; sourceTree = ""; }; + 3D03C6DA2AACE6AC00FDE5D2 /* hi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hi; path = hi.lproj/Intents.strings; sourceTree = ""; }; 4302F4E01D4E9C8900F0FCAF /* TextFieldTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldTableViewController.swift; sourceTree = ""; }; 4302F4E21D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InsulinDeliveryTableViewController.swift; sourceTree = ""; }; 430B29892041F54A00BA9F93 /* NSUserDefaults.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSUserDefaults.swift; sourceTree = ""; }; @@ -1180,6 +1183,7 @@ 84AA81E22A4A36FB000B658B /* SystemActionLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemActionLink.swift; sourceTree = ""; }; 84AA81E42A4A3981000B658B /* DeeplinkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeeplinkManager.swift; sourceTree = ""; }; 84AA81E62A4A4DEF000B658B /* PumpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpView.swift; sourceTree = ""; }; + 84D2879E2AC756C8007ED283 /* ContentMargin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentMargin.swift; sourceTree = ""; }; 891B508424342BE1005DA578 /* CarbAndBolusFlowViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbAndBolusFlowViewModel.swift; sourceTree = ""; }; 892A5D29222EF60A008961AB /* MockKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; name = MockKit.framework; path = Carthage/Build/iOS/MockKit.framework; sourceTree = SOURCE_ROOT; }; 892A5D2B222EF60A008961AB /* MockKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; name = MockKitUI.framework; path = Carthage/Build/iOS/MockKitUI.framework; sourceTree = SOURCE_ROOT; }; @@ -1298,6 +1302,7 @@ B42D124228D371C400E43D22 /* AlertMuter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertMuter.swift; sourceTree = ""; }; B43CF07D29434EC4008A520B /* HowMuteAlertWorkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HowMuteAlertWorkView.swift; sourceTree = ""; }; B43DA44024D9C12100CAFF4E /* DismissibleHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissibleHostingController.swift; sourceTree = ""; }; + B470F5832AB22B5100049695 /* StatefulPluggable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatefulPluggable.swift; sourceTree = ""; }; B48B0BAB24900093009A48DE /* PumpStatusHUDView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpStatusHUDView.swift; sourceTree = ""; }; B490A03C24D04F9400F509FA /* Color.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = ""; }; B490A03E24D0550F00F509FA /* GlucoseRangeCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseRangeCategory.swift; sourceTree = ""; }; @@ -1308,6 +1313,7 @@ B4CAD8782549D2540057946B /* LoopCompletionFreshnessTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopCompletionFreshnessTests.swift; sourceTree = ""; }; B4D4534028E5CA7900F1A8D9 /* AlertMuterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertMuterTests.swift; sourceTree = ""; }; B4D620D324D9EDB900043B3C /* GuidanceColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuidanceColors.swift; sourceTree = ""; }; + B4D904402AA8989100CBD826 /* StatefulPluginManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatefulPluginManager.swift; sourceTree = ""; }; B4E2022F2661063E009421B5 /* AutomaticDosingStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutomaticDosingStatus.swift; sourceTree = ""; }; B4E96D4A248A6B6E002DABAD /* DeviceStatusHUDView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStatusHUDView.swift; sourceTree = ""; }; B4E96D4E248A6E20002DABAD /* CGMStatusHUDView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGMStatusHUDView.swift; sourceTree = ""; }; @@ -1420,9 +1426,6 @@ C12CB9B623106A6200F84978 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/Intents.strings; sourceTree = ""; }; C12CB9B823106A6300F84978 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Intents.strings; sourceTree = ""; }; C13072B92A76AF31009A7C58 /* live_capture_predicted_glucose.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = live_capture_predicted_glucose.json; sourceTree = ""; }; - C13072BD2A76AF97009A7C58 /* live_capture_doses.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = live_capture_doses.json; sourceTree = ""; }; - C13072BF2A76B041009A7C58 /* live_capture_carb_entries.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = live_capture_carb_entries.json; sourceTree = ""; }; - C13072C32A76B0B1009A7C58 /* live_capture_historic_glucose.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = live_capture_historic_glucose.json; sourceTree = ""; }; C13DA2AF24F6C7690098BB29 /* UIViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewController.swift; sourceTree = ""; }; C148CEE624FD91BD00711B3B /* DeliveryUncertaintyAlertManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeliveryUncertaintyAlertManager.swift; sourceTree = ""; }; C14952142995822A0095AA84 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -1445,6 +1448,7 @@ C16B983D26B4893300256B05 /* DoseEnactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DoseEnactor.swift; sourceTree = ""; }; C16B983F26B4898800256B05 /* DoseEnactorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DoseEnactorTests.swift; sourceTree = ""; }; C16DA84122E8E112008624C2 /* PluginManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginManager.swift; sourceTree = ""; }; + C16FC0AF2A99392F0025E239 /* live_capture_input.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = live_capture_input.json; sourceTree = ""; }; C1742331259BEADC00399C9D /* ManualEntryDoseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManualEntryDoseView.swift; sourceTree = ""; }; C174233B259BEB0F00399C9D /* ManualEntryDoseViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManualEntryDoseViewModel.swift; sourceTree = ""; }; C174571329830930009EFCF2 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = ""; }; @@ -1542,6 +1546,7 @@ C1D0B62F2986D4D90098D215 /* LocalizedString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedString.swift; sourceTree = ""; }; C1D197FE232CF92D0096D646 /* capture-build-details.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = "capture-build-details.sh"; sourceTree = ""; }; C1D289B422F90A52003FFBD9 /* BasalDeliveryState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasalDeliveryState.swift; sourceTree = ""; }; + C1D476B32A8ED179002C1C87 /* LoopAlgorithmTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopAlgorithmTests.swift; sourceTree = ""; }; C1D70F7A2A914F71009FE129 /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = he.lproj/InfoPlist.strings; sourceTree = ""; }; C1DA986B2843B6F9001D04CC /* PersistedProperty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistedProperty.swift; sourceTree = ""; }; C1DE5D22251BFC4D00439E49 /* SimpleBolusView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SimpleBolusView.swift; sourceTree = ""; }; @@ -1565,7 +1570,7 @@ C1EB0D22299581D900628475 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/ckcomplication.strings; sourceTree = ""; }; C1EE9E802A38D0FB0064784A /* BuildDetails.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = BuildDetails.plist; sourceTree = ""; }; C1EF747128D6A44A00C8C083 /* CrashRecoveryManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashRecoveryManager.swift; sourceTree = ""; }; - C1F2075B26D6F9B0007AB7EB /* ProfileExpirationAlerter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileExpirationAlerter.swift; sourceTree = ""; }; + C1F2075B26D6F9B0007AB7EB /* AppExpirationAlerter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppExpirationAlerter.swift; sourceTree = ""; }; C1F48FF62995821600C8BD69 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = ""; }; C1F48FF72995821600C8BD69 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = ""; }; C1F48FF82995821600C8BD69 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; @@ -1864,6 +1869,7 @@ E9B3552C293592B40076AB04 /* MealDetectionManagerTests.swift */, 1D70C40026EC0F9D00C62570 /* SupportManagerTests.swift */, A9F5F1F4251050EC00E7C8A4 /* ZipArchiveTests.swift */, + C1D476B32A8ED179002C1C87 /* LoopAlgorithmTests.swift */, ); path = Managers; sourceTree = ""; @@ -1993,7 +1999,7 @@ 43D9FFCF21EAE05D00AF44BF /* LoopCore.framework */, 43D9002A21EB209400AF44BF /* LoopCore.framework */, E9B07F7C253BBA6500BAD8F8 /* Loop Intent Extension.appex */, - 14B1735C28AED9EC006CCD7C /* SmallStatusWidgetExtension.appex */, + 14B1735C28AED9EC006CCD7C /* Loop Widget Extension.appex */, ); name = Products; sourceTree = ""; @@ -2294,10 +2300,12 @@ 43C094491CACCC73001F6403 /* NotificationManager.swift */, A97F250725E056D500F0EE19 /* OnboardingManager.swift */, 432E73CA1D24B3D6009AD15D /* RemoteDataServicesManager.swift */, + B4D904402AA8989100CBD826 /* StatefulPluginManager.swift */, A9C62D852331703000535612 /* Service.swift */, A9C62D872331703000535612 /* ServicesManager.swift */, C1F7822527CC056900C0919A /* SettingsManager.swift */, E9BB27AA23B85C3500FB4987 /* SleepStore.swift */, + B470F5832AB22B5100049695 /* StatefulPluggable.swift */, 43FCEEA8221A615B0013DD30 /* StatusChartsManager.swift */, 1D63DEA426E950D400F46FA5 /* SupportManager.swift */, 4F70C20F1DE8FAC5006380B7 /* ExtensionDataManager.swift */, @@ -2307,7 +2315,7 @@ 1DA6499D2441266400F61E75 /* Alerts */, E95D37FF24EADE68005E2F50 /* Store Protocols */, E9B355232935906B0076AB04 /* Missed Meal Detection */, - C1F2075B26D6F9B0007AB7EB /* ProfileExpirationAlerter.swift */, + C1F2075B26D6F9B0007AB7EB /* AppExpirationAlerter.swift */, A96DAC2B2838F31200D94E38 /* SharedLogging.swift */, 7E69CFFB2A16A77E00203CBD /* ResetLoopManager.swift */, 84AA81E42A4A3981000B658B /* DeeplinkManager.swift */, @@ -2526,6 +2534,7 @@ children = ( 84AA81DA2A4A2973000B658B /* Date.swift */, 84AA81D52A4A28AF000B658B /* WidgetBackground.swift */, + 84D2879E2AC756C8007ED283 /* ContentMargin.swift */, ); path = Helpers; sourceTree = ""; @@ -2755,9 +2764,7 @@ isa = PBXGroup; children = ( C13072B92A76AF31009A7C58 /* live_capture_predicted_glucose.json */, - C13072BD2A76AF97009A7C58 /* live_capture_doses.json */, - C13072BF2A76B041009A7C58 /* live_capture_carb_entries.json */, - C13072C32A76B0B1009A7C58 /* live_capture_historic_glucose.json */, + C16FC0AF2A99392F0025E239 /* live_capture_input.json */, ); path = live_capture; sourceTree = ""; @@ -2983,7 +2990,7 @@ ); name = "Loop Widget Extension"; productName = SmallStatusWidgetExtension; - productReference = 14B1735C28AED9EC006CCD7C /* SmallStatusWidgetExtension.appex */; + productReference = 14B1735C28AED9EC006CCD7C /* Loop Widget Extension.appex */; productType = "com.apple.product-type.app-extension"; }; 43776F8B1B8022E90074EA36 /* Loop */ = { @@ -3412,7 +3419,6 @@ E93E86CE24E2E02200FF40C8 /* high_and_stable_momentum_effect.json in Resources */, C13072BA2A76AF31009A7C58 /* live_capture_predicted_glucose.json in Resources */, E93E865424DB6CBA00FF40C8 /* retrospective_output.json in Resources */, - C13072BE2A76AF97009A7C58 /* live_capture_doses.json in Resources */, E9C58A7F24DB529A00487A17 /* counteraction_effect_falling_glucose.json in Resources */, E93E865624DB731900FF40C8 /* predicted_glucose_without_retrospective.json in Resources */, E9B3553D293706CB0076AB04 /* long_interval_counteraction_effect.json in Resources */, @@ -3425,12 +3431,12 @@ E90909D224E34AC500F963D2 /* high_and_rising_with_cob_insulin_effect.json in Resources */, E90909F224E35B4D00F963D2 /* high_and_falling_counteraction_effect.json in Resources */, E90909EE24E35B4000F963D2 /* high_and_falling_predicted_glucose.json in Resources */, - C13072C02A76B041009A7C58 /* live_capture_carb_entries.json in Resources */, E90909DD24E34F1600F963D2 /* low_and_falling_carb_effect.json in Resources */, E90909E924E3530200F963D2 /* low_with_low_treatment_predicted_glucose.json in Resources */, E90909D124E34AC500F963D2 /* high_and_rising_with_cob_momentum_effect.json in Resources */, E9C58A8024DB529A00487A17 /* insulin_effect.json in Resources */, E90909DF24E34F1600F963D2 /* low_and_falling_insulin_effect.json in Resources */, + C16FC0B02A99392F0025E239 /* live_capture_input.json in Resources */, E93E86BE24E1FDC400FF40C8 /* flat_and_stable_carb_effect.json in Resources */, E90909E824E3530200F963D2 /* low_with_low_treatment_insulin_effect.json in Resources */, E9B3553B293706CB0076AB04 /* noisy_cgm_counteraction_effect.json in Resources */, @@ -3451,7 +3457,6 @@ E93E86C324E1FE6100FF40C8 /* flat_and_stable_counteraction_effect.json in Resources */, E9C58A7E24DB529A00487A17 /* dynamic_glucose_effect_partially_observed.json in Resources */, E90909F324E35B4D00F963D2 /* high_and_falling_carb_effect.json in Resources */, - C13072C42A76B0B1009A7C58 /* live_capture_historic_glucose.json in Resources */, E93E86CA24E2E02200FF40C8 /* high_and_stable_insulin_effect.json in Resources */, E93E86BB24E1FDC400FF40C8 /* flat_and_stable_momentum_effect.json in Resources */, E93E86CB24E2E02200FF40C8 /* high_and_stable_carb_effect.json in Resources */, @@ -3519,6 +3524,7 @@ }; C113F4472951352C00758735 /* Install Scenarios */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -3537,6 +3543,7 @@ }; C16DA84322E8E5FF008624C2 /* Install Plugins */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -3555,6 +3562,7 @@ }; C1D1405722FB66DF00DA6242 /* Build Derived Assets */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -3638,6 +3646,7 @@ 14B1737328AEDBF6006CCD7C /* SystemStatusWidget.swift in Sources */, 84AA81DD2A4A2999000B658B /* StatusWidgetTimelineProvider.swift in Sources */, 14B1737528AEDBF6006CCD7C /* LoopCircleView.swift in Sources */, + 84D2879F2AC756C8007ED283 /* ContentMargin.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3654,14 +3663,16 @@ C1D289B522F90A52003FFBD9 /* BasalDeliveryState.swift in Sources */, 4F2C15821E074FC600E160D4 /* NSTimeInterval.swift in Sources */, 4311FB9B1F37FE1B00D4C0A7 /* TitleSubtitleTextFieldTableViewCell.swift in Sources */, - C1F2075C26D6F9B0007AB7EB /* ProfileExpirationAlerter.swift in Sources */, + C1F2075C26D6F9B0007AB7EB /* AppExpirationAlerter.swift in Sources */, B4FEEF7D24B8A71F00A8DF9B /* DeviceDataManager+DeviceStatus.swift in Sources */, 142CB7592A60BF2E0075748A /* EditMode.swift in Sources */, E95D380324EADF36005E2F50 /* CarbStoreProtocol.swift in Sources */, + B470F5842AB22B5100049695 /* StatefulPluggable.swift in Sources */, E98A55ED24EDD6380008715D /* LatestStoredSettingsProvider.swift in Sources */, C1FB428F217921D600FAB378 /* PumpManagerUI.swift in Sources */, A9B996F227238705002DC09C /* DosingDecisionStore.swift in Sources */, 43C513191E864C4E001547C7 /* GlucoseRangeSchedule.swift in Sources */, + B4D904412AA8989100CBD826 /* StatefulPluginManager.swift in Sources */, E9B355292935919E0076AB04 /* MissedMealSettings.swift in Sources */, 43A51E1F1EB6D62A000736CC /* CarbAbsorptionViewController.swift in Sources */, 43776F901B8022E90074EA36 /* AppDelegate.swift in Sources */, @@ -3991,6 +4002,7 @@ E93E86A824DDCC4400FF40C8 /* MockDoseStore.swift in Sources */, B4D4534128E5CA7900F1A8D9 /* AlertMuterTests.swift in Sources */, E98A55F124EDD85E0008715D /* MockDosingDecisionStore.swift in Sources */, + C1D476B42A8ED179002C1C87 /* LoopAlgorithmTests.swift in Sources */, 8968B114240C55F10074BB48 /* LoopSettingsTests.swift in Sources */, A9BD28E7272226B40071DF15 /* TestLocalizedError.swift in Sources */, A9F5F1F5251050EC00E7C8A4 /* ZipArchiveTests.swift in Sources */, @@ -4228,6 +4240,7 @@ C1C3127F297E4C0400296DA4 /* ar */, C1C247882995823200371B88 /* sk */, C1C5357529C6346A00E32DF9 /* cs */, + 3D03C6DA2AACE6AC00FDE5D2 /* hi */, ); name = Intents.intentdefinition; sourceTree = ""; @@ -4829,6 +4842,10 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "$(LOOP_PROVISIONING_PROFILE_SPECIFIER_WIDGET_EXTENSION_DEBUG)"; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -4873,6 +4890,10 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "$(LOOP_PROVISIONING_PROFILE_SPECIFIER_WIDGET_EXTENSION_RELEASE)"; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; @@ -5130,6 +5151,7 @@ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; TARGETED_DEVICE_FAMILY = 1; }; name = Debug; @@ -5156,6 +5178,7 @@ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; TARGETED_DEVICE_FAMILY = 1; }; name = Release; @@ -5409,6 +5432,10 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "$(LOOP_PROVISIONING_PROFILE_SPECIFIER_STATUS_EXTENSION_DEBUG)"; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; TARGETED_DEVICE_FAMILY = 1; }; name = Debug; @@ -5431,6 +5458,10 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "$(LOOP_PROVISIONING_PROFILE_SPECIFIER_STATUS_EXTENSION_RELEASE)"; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; TARGETED_DEVICE_FAMILY = 1; }; name = Release; @@ -5494,6 +5525,10 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "$(LOOP_PROVISIONING_PROFILE_SPECIFIER_INTENT_EXTENSION_DEBUG)"; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; TARGETED_DEVICE_FAMILY = 1; }; name = Debug; @@ -5517,6 +5552,10 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "$(LOOP_PROVISIONING_PROFILE_SPECIFIER_INTENT_EXTENSION_RELEASE)"; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; TARGETED_DEVICE_FAMILY = 1; }; name = Release; diff --git a/Loop.xcodeproj/xcshareddata/xcschemes/DoseMathTests.xcscheme b/Loop.xcodeproj/xcshareddata/xcschemes/DoseMathTests.xcscheme index f225f4098a..a56f874c88 100644 --- a/Loop.xcodeproj/xcshareddata/xcschemes/DoseMathTests.xcscheme +++ b/Loop.xcodeproj/xcshareddata/xcschemes/DoseMathTests.xcscheme @@ -1,6 +1,6 @@ expirationAlertWindow { + return + } + + let minimumTimeBetweenAlerts: TimeInterval = timeUntilExpiration > .hours(24) ? .days(2) : .hours(1) + + if let lastAlertDate = UserDefaults.appGroup?.lastProfileExpirationAlertDate { + guard now > lastAlertDate + minimumTimeBetweenAlerts else { + return + } + } + + let formatter = DateComponentsFormatter() + formatter.allowedUnits = [.day, .hour] + formatter.unitsStyle = .full + formatter.zeroFormattingBehavior = .dropLeading + formatter.maximumUnitCount = 1 + let timeUntilExpirationStr = formatter.string(from: timeUntilExpiration) + + let alertMessage = createVerboseAlertMessage(timeUntilExpirationStr: timeUntilExpirationStr!) + + var dialog: UIAlertController + if isTestFlightBuild() { + dialog = UIAlertController( + title: NSLocalizedString("TestFlight Expires Soon", comment: "The title for notification of upcoming TestFlight expiration"), + message: alertMessage, + preferredStyle: .alert) + dialog.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Text for ok action on notification of upcoming TestFlight expiration"), style: .default, handler: nil)) + dialog.addAction(UIAlertAction(title: NSLocalizedString("More Info", comment: "Text for more info action on notification of upcoming TestFlight expiration"), style: .default, handler: { (_) in + UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/gh-actions/gh-update/")!) + })) + + } else { + dialog = UIAlertController( + title: NSLocalizedString("Profile Expires Soon", comment: "The title for notification of upcoming profile expiration"), + message: alertMessage, + preferredStyle: .alert) + dialog.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Text for ok action on notification of upcoming profile expiration"), style: .default, handler: nil)) + dialog.addAction(UIAlertAction(title: NSLocalizedString("More Info", comment: "Text for more info action on notification of upcoming profile expiration"), style: .default, handler: { (_) in + UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/build/updating/")!) + })) + } + viewControllerToPresentFrom.present(dialog, animated: true, completion: nil) + + UserDefaults.appGroup?.lastProfileExpirationAlertDate = now + } + + static func createVerboseAlertMessage(timeUntilExpirationStr:String) -> String { + if isTestFlightBuild() { + return String(format: NSLocalizedString("%1$@ will stop working in %2$@. You will need to rebuild before that.", comment: "Format string for body for notification of upcoming expiration. (1: app name) (2: amount of time until expiration"), Bundle.main.bundleDisplayName, timeUntilExpirationStr) + } else { + return String(format: NSLocalizedString("%1$@ will stop working in %2$@. You will need to update before that, with a new provisioning profile.", comment: "Format string for body for notification of upcoming provisioning profile expiration. (1: app name) (2: amount of time until expiration"), Bundle.main.bundleDisplayName, timeUntilExpirationStr) + } + } + + static func isNearExpiration(expirationDate:Date) -> Bool { + return expirationDate.timeIntervalSinceNow < settingsPageExpirationWarningModeWindow + } + + static func createProfileExpirationSettingsMessage(expirationDate:Date) -> String { + let nearExpiration = isNearExpiration(expirationDate: expirationDate) + let maxUnitCount = nearExpiration ? 2 : 1 // only include hours in the msg if near expiration + let readableRelativeTime: String? = relativeTimeFormatter(maxUnitCount: maxUnitCount).string(from: expirationDate.timeIntervalSinceNow) + let relativeTimeRemaining: String = readableRelativeTime ?? NSLocalizedString("Unknown time", comment: "Unknown amount of time in settings' profile expiration section") + let verboseMessage = createVerboseAlertMessage(timeUntilExpirationStr: relativeTimeRemaining) + let conciseMessage = relativeTimeRemaining + NSLocalizedString(" remaining", comment: "remaining time in setting's profile expiration section") + return nearExpiration ? verboseMessage : conciseMessage + } + + private static func relativeTimeFormatter(maxUnitCount:Int) -> DateComponentsFormatter { + let formatter = DateComponentsFormatter() + let includeHours = maxUnitCount == 2 + formatter.allowedUnits = includeHours ? [.day, .hour] : [.day] + formatter.unitsStyle = .full + formatter.zeroFormattingBehavior = .dropLeading + formatter.maximumUnitCount = maxUnitCount + return formatter + } + + static func buildDate() -> Date? { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "EEE MMM d HH:mm:ss 'UTC' yyyy" + dateFormatter.locale = Locale(identifier: "en_US_POSIX") // Set locale to ensure parsing works + dateFormatter.timeZone = TimeZone(identifier: "UTC") + + guard let dateString = BuildDetails.default.buildDateString, + let date = dateFormatter.date(from: dateString) else { + return nil + } + + return date + } + + static func isTestFlightBuild() -> Bool { + // If the target environment is a simulator, then + // this is not a TestFlight distribution. Return false. + #if targetEnvironment(simulator) + return false + #else + + // If an "embedded.mobileprovision" is present in the main bundle, then + // this is an Xcode, Ad-Hoc, or Enterprise distribution. Return false. + if Bundle.main.url(forResource: "embedded", withExtension: "mobileprovision") != nil { + return false + } + + // If an app store receipt is not present in the main bundle, then we cannot + // say whether this is a TestFlight or App Store distribution. Return false. + guard let receiptName = Bundle.main.appStoreReceiptURL?.lastPathComponent else { + return false + } + + // A TestFlight distribution presents a "sandboxReceipt", while an App Store + // distribution presents a "receipt". Return true if we have a TestFlight receipt. + return "sandboxReceipt".caseInsensitiveCompare(receiptName) == .orderedSame + #endif + } + + static func calculateExpirationDate(profileExpiration: Date) -> Date { + let isTestFlight = isTestFlightBuild() + + if isTestFlight, let buildDate = buildDate() { + let testflightExpiration = Calendar.current.date(byAdding: .day, value: 90, to: buildDate)! + + return testflightExpiration + } else { + return profileExpiration + } + } +} diff --git a/Loop/Managers/CGMManager.swift b/Loop/Managers/CGMManager.swift index 041e632288..fe39e3926c 100644 --- a/Loop/Managers/CGMManager.swift +++ b/Loop/Managers/CGMManager.swift @@ -10,13 +10,13 @@ import LoopKitUI import MockKit let staticCGMManagersByIdentifier: [String: CGMManager.Type] = [ - MockCGMManager.managerIdentifier: MockCGMManager.self + MockCGMManager.pluginIdentifier: MockCGMManager.self ] var availableStaticCGMManagers: [CGMManagerDescriptor] { if FeatureFlags.allowSimulators { return [ - CGMManagerDescriptor(identifier: MockCGMManager.managerIdentifier, localizedTitle: MockCGMManager.localizedTitle) + CGMManagerDescriptor(identifier: MockCGMManager.pluginIdentifier, localizedTitle: MockCGMManager.localizedTitle) ] } else { return [] @@ -40,7 +40,7 @@ extension CGMManager { var rawValue: [String: Any] { return [ - "managerIdentifier": managerIdentifier, + "managerIdentifier": pluginIdentifier, "state": self.rawState ] } diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 2e8d157531..2751f18f50 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -97,9 +97,9 @@ final class DeviceDataManager { dispatchPrecondition(condition: .onQueue(.main)) setupCGM() - if cgmManager?.managerIdentifier != oldValue?.managerIdentifier { + if cgmManager?.pluginIdentifier != oldValue?.pluginIdentifier { if let cgmManager = cgmManager { - analyticsServicesManager.cgmWasAdded(identifier: cgmManager.managerIdentifier) + analyticsServicesManager.cgmWasAdded(identifier: cgmManager.pluginIdentifier) } else { analyticsServicesManager.cgmWasRemoved() } @@ -125,9 +125,9 @@ final class DeviceDataManager { cgmManager = nil } - if pumpManager?.managerIdentifier != oldValue?.managerIdentifier { + if pumpManager?.pluginIdentifier != oldValue?.pluginIdentifier { if let pumpManager = pumpManager { - analyticsServicesManager.pumpWasAdded(identifier: pumpManager.managerIdentifier) + analyticsServicesManager.pumpWasAdded(identifier: pumpManager.pluginIdentifier) } else { analyticsServicesManager.pumpWasRemoved() } @@ -157,6 +157,8 @@ final class DeviceDataManager { let glucoseStore: GlucoseStore + let cgmEventStore: CgmEventStore + private let cacheStore: PersistenceController let dosingDecisionStore: DosingDecisionStore @@ -205,6 +207,8 @@ final class DeviceDataManager { sleepDataAuthorizationRequired } + private(set) var statefulPluginManager: StatefulPluginManager! + // MARK: Services private(set) var servicesManager: ServicesManager! @@ -337,11 +341,13 @@ final class DeviceDataManager { cgmStalenessMonitor = CGMStalenessMonitor() cgmStalenessMonitor.delegate = glucoseStore + + cgmEventStore = CgmEventStore(cacheStore: cacheStore, cacheLength: localCacheDuration) + + dosingDecisionStore = DosingDecisionStore(store: cacheStore, expireAfter: localCacheDuration) - self.dosingDecisionStore = DosingDecisionStore(store: cacheStore, expireAfter: localCacheDuration) - - self.cgmHasValidSensorSession = false - self.pumpIsAllowingAutomation = true + cgmHasValidSensorSession = false + pumpIsAllowingAutomation = true self.automaticDosingStatus = automaticDosingStatus // HealthStorePreferredGlucoseUnitDidChange will be notified once the user completes the health access form. Set to .milligramsPerDeciliter until then @@ -405,6 +411,7 @@ final class DeviceDataManager { doseStore: doseStore, dosingDecisionStore: dosingDecisionStore, glucoseStore: glucoseStore, + cgmEventStore: cgmEventStore, settingsStore: settingsManager.settingsStore, overrideHistory: overrideHistory, insulinDeliveryStore: doseStore.insulinDeliveryStore @@ -422,7 +429,9 @@ final class DeviceDataManager { servicesManagerDelegate: loopManager, servicesManagerDosingDelegate: self ) - + + statefulPluginManager = StatefulPluginManager(pluginManager: pluginManager, servicesManager: servicesManager) + let criticalEventLogs: [CriticalEventLog] = [settingsManager.settingsStore, glucoseStore, carbStore, dosingDecisionStore, doseStore, deviceLog, alertManager.alertStore] criticalEventLogExportManager = CriticalEventLogExportManager(logs: criticalEventLogs, directory: FileManager.default.exportsDirectoryURL, @@ -435,6 +444,7 @@ final class DeviceDataManager { doseStore.delegate = self dosingDecisionStore.delegate = self glucoseStore.delegate = self + cgmEventStore.delegate = self doseStore.insulinDeliveryStore.delegate = self remoteDataServicesManager.delegate = self @@ -582,7 +592,7 @@ final class DeviceDataManager { var availableCGMManagers: [CGMManagerDescriptor] { var availableCGMManagers = pluginManager.availableCGMManagers + availableStaticCGMManagers if let pumpManagerAsCGMManager = pumpManager as? CGMManager { - availableCGMManagers.append(CGMManagerDescriptor(identifier: pumpManagerAsCGMManager.managerIdentifier, localizedTitle: pumpManagerAsCGMManager.localizedTitle)) + availableCGMManagers.append(CGMManagerDescriptor(identifier: pumpManagerAsCGMManager.pluginIdentifier, localizedTitle: pumpManagerAsCGMManager.localizedTitle)) } availableCGMManagers = availableCGMManagers.filter({ cgmManager in @@ -635,7 +645,7 @@ final class DeviceDataManager { } public func setupCGMManagerFromPumpManager(withIdentifier identifier: String) -> CGMManager? { - guard identifier == pumpManager?.managerIdentifier, let cgmManager = pumpManager as? CGMManager else { + guard identifier == pumpManager?.pluginIdentifier, let cgmManager = pumpManager as? CGMManager else { return nil } @@ -698,19 +708,20 @@ private extension DeviceDataManager { cgmManager?.cgmManagerDelegate = self cgmManager?.delegateQueue = queue + reportPluginInitializationComplete() glucoseStore.managedDataInterval = cgmManager?.managedDataInterval glucoseStore.healthKitStorageDelay = cgmManager.map{ type(of: $0).healthKitStorageDelay } ?? 0 updatePumpManagerBLEHeartbeatPreference() if let cgmManager = cgmManager { - alertManager?.addAlertResponder(managerIdentifier: cgmManager.managerIdentifier, + alertManager?.addAlertResponder(managerIdentifier: cgmManager.pluginIdentifier, alertResponder: cgmManager) - alertManager?.addAlertSoundVendor(managerIdentifier: cgmManager.managerIdentifier, + alertManager?.addAlertSoundVendor(managerIdentifier: cgmManager.pluginIdentifier, soundVendor: cgmManager) cgmHasValidSensorSession = cgmManager.cgmManagerStatus.hasValidSensorSession - analyticsServicesManager.identifyCGMType(cgmManager.managerIdentifier) + analyticsServicesManager.identifyCGMType(cgmManager.pluginIdentifier) } if let cgmManagerUI = cgmManager as? CGMManagerUI { @@ -723,6 +734,7 @@ private extension DeviceDataManager { pumpManager?.pumpManagerDelegate = self pumpManager?.delegateQueue = queue + reportPluginInitializationComplete() doseStore.device = pumpManager?.status.device pumpManagerHUDProvider = pumpManager?.hudProvider(bluetoothProvider: bluetoothProvider, colorPalette: .default, allowedInsulinTypes: allowedInsulinTypes) @@ -732,14 +744,16 @@ private extension DeviceDataManager { doseStore.pumpRecordsBasalProfileStartEvents = pumpRecordsBasalProfileStartEvents } if let pumpManager = pumpManager { - alertManager?.addAlertResponder(managerIdentifier: pumpManager.managerIdentifier, + alertManager?.addAlertResponder(managerIdentifier: pumpManager.pluginIdentifier, alertResponder: pumpManager) - alertManager?.addAlertSoundVendor(managerIdentifier: pumpManager.managerIdentifier, + alertManager?.addAlertSoundVendor(managerIdentifier: pumpManager.pluginIdentifier, soundVendor: pumpManager) deliveryUncertaintyAlertManager = DeliveryUncertaintyAlertManager(pumpManager: pumpManager, alertPresenter: alertPresenter) - analyticsServicesManager.identifyPumpType(pumpManager.managerIdentifier) + analyticsServicesManager.identifyPumpType(pumpManager.pluginIdentifier) + + updatePumpManagerBLEHeartbeatPreference() } } @@ -750,6 +764,58 @@ private extension DeviceDataManager { } } +// MARK: - Plugins +extension DeviceDataManager { + func reportPluginInitializationComplete() { + let allActivePlugins = self.allActivePlugins + + for plugin in servicesManager.activeServices { + plugin.initializationComplete(for: allActivePlugins) + } + + for plugin in statefulPluginManager.activeStatefulPlugins { + plugin.initializationComplete(for: allActivePlugins) + } + + for plugin in availableSupports { + plugin.initializationComplete(for: allActivePlugins) + } + + cgmManager?.initializationComplete(for: allActivePlugins) + pumpManager?.initializationComplete(for: allActivePlugins) + } + + var allActivePlugins: [Pluggable] { + var allActivePlugins: [Pluggable] = servicesManager.activeServices + + for plugin in statefulPluginManager.activeStatefulPlugins { + if !allActivePlugins.contains(where: { $0.pluginIdentifier == plugin.pluginIdentifier }) { + allActivePlugins.append(plugin) + } + } + + for plugin in availableSupports { + if !allActivePlugins.contains(where: { $0.pluginIdentifier == plugin.pluginIdentifier }) { + allActivePlugins.append(plugin) + } + } + + if let cgmManager = cgmManager { + if !allActivePlugins.contains(where: { $0.pluginIdentifier == cgmManager.pluginIdentifier }) { + allActivePlugins.append(cgmManager) + } + } + + if let pumpManager = pumpManager { + if !allActivePlugins.contains(where: { $0.pluginIdentifier == pumpManager.pluginIdentifier }) { + allActivePlugins.append(pumpManager) + } + } + + return allActivePlugins + } +} + // MARK: - Client API extension DeviceDataManager { func enactBolus(units: Double, activationType: BolusActivationType, completion: @escaping (_ error: Error?) -> Void = { _ in }) { @@ -861,7 +927,7 @@ extension DeviceDataManager { extension DeviceDataManager: DeviceManagerDelegate { func deviceManager(_ manager: DeviceManager, logEventForDeviceIdentifier deviceIdentifier: String?, type: DeviceLogEntryType, message: String, completion: ((Error?) -> Void)?) { - deviceLog.log(managerIdentifier: manager.managerIdentifier, deviceIdentifier: deviceIdentifier, type: type, message: message, completion: completion) + deviceLog.log(managerIdentifier: manager.pluginIdentifier, deviceIdentifier: deviceIdentifier, type: type, message: message, completion: completion) } var allowDebugFeatures: Bool { @@ -909,7 +975,7 @@ extension DeviceDataManager: CGMManagerDelegate { func cgmManagerWantsDeletion(_ manager: CGMManager) { dispatchPrecondition(condition: .onQueue(queue)) - log.default("CGM manager with identifier '%{public}@' wants deletion", manager.managerIdentifier) + log.default("CGM manager with identifier '%{public}@' wants deletion", manager.pluginIdentifier) DispatchQueue.main.async { if let cgmManagerUI = self.cgmManager as? CGMManagerUI { @@ -934,6 +1000,16 @@ extension DeviceDataManager: CGMManagerDelegate { } } + func cgmManager(_ manager: LoopKit.CGMManager, hasNew events: [PersistedCgmEvent]) { + Task { + do { + try await cgmEventStore.add(events: events) + } catch { + self.log.error("Error storing cgm events: %{public}@", error.localizedDescription) + } + } + } + func startDateToFilterNewData(for manager: CGMManager) -> Date? { dispatchPrecondition(condition: .onQueue(queue)) return glucoseStore.latestGlucose?.startDate @@ -962,13 +1038,13 @@ extension DeviceDataManager: CGMManagerDelegate { extension DeviceDataManager: CGMManagerOnboardingDelegate { func cgmManagerOnboarding(didCreateCGMManager cgmManager: CGMManagerUI) { - log.default("CGM manager with identifier '%{public}@' created", cgmManager.managerIdentifier) + log.default("CGM manager with identifier '%{public}@' created", cgmManager.pluginIdentifier) self.cgmManager = cgmManager } func cgmManagerOnboarding(didOnboardCGMManager cgmManager: CGMManagerUI) { precondition(cgmManager.isOnboarded) - log.default("CGM manager with identifier '%{public}@' onboarded", cgmManager.managerIdentifier) + log.default("CGM manager with identifier '%{public}@' onboarded", cgmManager.pluginIdentifier) DispatchQueue.main.async { self.refreshDeviceData() @@ -1101,7 +1177,7 @@ extension DeviceDataManager: PumpManagerDelegate { func pumpManagerWillDeactivate(_ pumpManager: PumpManager) { dispatchPrecondition(condition: .onQueue(queue)) - log.default("Pump manager with identifier '%{public}@' will deactivate", pumpManager.managerIdentifier) + log.default("Pump manager with identifier '%{public}@' will deactivate", pumpManager.pluginIdentifier) DispatchQueue.main.async { self.pumpManager = nil @@ -1124,11 +1200,17 @@ extension DeviceDataManager: PumpManagerDelegate { setLastError(error: error) } - func pumpManager(_ pumpManager: PumpManager, hasNewPumpEvents events: [NewPumpEvent], lastReconciliation: Date?, completion: @escaping (_ error: Error?) -> Void) { + func pumpManager( + _ pumpManager: PumpManager, + hasNewPumpEvents events: [NewPumpEvent], + lastReconciliation: Date?, + replacePendingEvents: Bool, + completion: @escaping (_ error: Error?) -> Void) + { dispatchPrecondition(condition: .onQueue(queue)) log.default("PumpManager:%{public}@ hasNewPumpEvents (lastReconciliation = %{public}@)", String(describing: type(of: pumpManager)), String(describing: lastReconciliation)) - loopManager.addPumpEvents(events, lastReconciliation: lastReconciliation) { (error) in + doseStore.addPumpEvents(events, lastReconciliation: lastReconciliation, replacePendingEvents: replacePendingEvents) { (error) in if let error = error { self.log.error("Failed to addPumpEvents to DoseStore: %{public}@", String(describing: error)) } @@ -1170,13 +1252,13 @@ extension DeviceDataManager: PumpManagerDelegate { extension DeviceDataManager: PumpManagerOnboardingDelegate { func pumpManagerOnboarding(didCreatePumpManager pumpManager: PumpManagerUI) { - log.default("Pump manager with identifier '%{public}@' created", pumpManager.managerIdentifier) + log.default("Pump manager with identifier '%{public}@' created", pumpManager.pluginIdentifier) self.pumpManager = pumpManager } func pumpManagerOnboarding(didOnboardPumpManager pumpManager: PumpManagerUI) { precondition(pumpManager.isOnboarded) - log.default("Pump manager with identifier '%{public}@' onboarded", pumpManager.managerIdentifier) + log.default("Pump manager with identifier '%{public}@' onboarded", pumpManager.pluginIdentifier) DispatchQueue.main.async { self.refreshDeviceData() @@ -1191,60 +1273,56 @@ extension DeviceDataManager: PumpManagerOnboardingDelegate { // MARK: - AlertStoreDelegate extension DeviceDataManager: AlertStoreDelegate { - func alertStoreHasUpdatedAlertData(_ alertStore: AlertStore) { - remoteDataServicesManager.alertStoreHasUpdatedAlertData(alertStore) + remoteDataServicesManager.triggerUpload(for: .alert) } - } // MARK: - CarbStoreDelegate extension DeviceDataManager: CarbStoreDelegate { - func carbStoreHasUpdatedCarbData(_ carbStore: CarbStore) { - remoteDataServicesManager.carbStoreHasUpdatedCarbData(carbStore) + remoteDataServicesManager.triggerUpload(for: .carb) } func carbStore(_ carbStore: CarbStore, didError error: CarbStore.CarbStoreError) {} - } // MARK: - DoseStoreDelegate extension DeviceDataManager: DoseStoreDelegate { - func doseStoreHasUpdatedPumpEventData(_ doseStore: DoseStore) { - remoteDataServicesManager.doseStoreHasUpdatedPumpEventData(doseStore) + remoteDataServicesManager.triggerUpload(for: .pumpEvent) } - } // MARK: - DosingDecisionStoreDelegate extension DeviceDataManager: DosingDecisionStoreDelegate { - func dosingDecisionStoreHasUpdatedDosingDecisionData(_ dosingDecisionStore: DosingDecisionStore) { - remoteDataServicesManager.dosingDecisionStoreHasUpdatedDosingDecisionData(dosingDecisionStore) + remoteDataServicesManager.triggerUpload(for: .dosingDecision) } - } // MARK: - GlucoseStoreDelegate extension DeviceDataManager: GlucoseStoreDelegate { - func glucoseStoreHasUpdatedGlucoseData(_ glucoseStore: GlucoseStore) { - remoteDataServicesManager.glucoseStoreHasUpdatedGlucoseData(glucoseStore) + remoteDataServicesManager.triggerUpload(for: .glucose) } - } // MARK: - InsulinDeliveryStoreDelegate extension DeviceDataManager: InsulinDeliveryStoreDelegate { - func insulinDeliveryStoreHasUpdatedDoseData(_ insulinDeliveryStore: InsulinDeliveryStore) { - remoteDataServicesManager.insulinDeliveryStoreHasUpdatedDoseData(insulinDeliveryStore) + remoteDataServicesManager.triggerUpload(for: .dose) } +} +// MARK: - CgmEventStoreDelegate +extension DeviceDataManager: CgmEventStoreDelegate { + func cgmEventStoreHasUpdatedData(_ cgmEventStore: LoopKit.CgmEventStore) { + remoteDataServicesManager.triggerUpload(for: .cgmEvent) + } } + // MARK: - TestingPumpManager extension DeviceDataManager { func deleteTestingPumpData(completion: ((Error?) -> Void)? = nil) { diff --git a/Loop/Managers/LoggingServicesManager.swift b/Loop/Managers/LoggingServicesManager.swift index 25b63ac1f9..287371aa01 100644 --- a/Loop/Managers/LoggingServicesManager.swift +++ b/Loop/Managers/LoggingServicesManager.swift @@ -24,7 +24,7 @@ final class LoggingServicesManager: Logging { } func removeService(_ loggingService: LoggingService) { - loggingServices.removeAll { $0.serviceIdentifier == loggingService.serviceIdentifier } + loggingServices.removeAll { $0.pluginIdentifier == loggingService.pluginIdentifier } } func log (_ message: StaticString, subsystem: String, category: String, type: OSLogType, _ args: [CVarArg]) { diff --git a/Loop/Managers/LoopAppManager.swift b/Loop/Managers/LoopAppManager.swift index 43c62d128b..b8e23d0bba 100644 --- a/Loop/Managers/LoopAppManager.swift +++ b/Loop/Managers/LoopAppManager.swift @@ -226,6 +226,7 @@ class LoopAppManager: NSObject { onboardingManager = OnboardingManager(pluginManager: pluginManager, bluetoothProvider: bluetoothStateManager, deviceDataManager: deviceDataManager, + statefulPluginManager: deviceDataManager.statefulPluginManager, servicesManager: deviceDataManager.servicesManager, loopDataManager: deviceDataManager.loopManager, supportManager: supportManager, @@ -238,11 +239,8 @@ class LoopAppManager: NSObject { if let analyticsService = support as? AnalyticsService { analyticsServicesManager.addService(analyticsService) } + support.initializationComplete(for: deviceDataManager.allActivePlugins) } - for support in supportManager.availableSupports { - support.initializationComplete(for: deviceDataManager.servicesManager.activeServices) - } - deviceDataManager.onboardingManager = onboardingManager @@ -254,7 +252,7 @@ class LoopAppManager: NSObject { } analyticsServicesManager.identify("Dosing Strategy", value: settingsManager.loopSettings.automaticDosingStrategy.analyticsValue) - let serviceNames = deviceDataManager.servicesManager.activeServices.map { $0.serviceIdentifier } + let serviceNames = deviceDataManager.servicesManager.activeServices.map { $0.pluginIdentifier } analyticsServicesManager.identify("Services", array: serviceNames) if FeatureFlags.scenariosEnabled { @@ -323,7 +321,7 @@ class LoopAppManager: NSObject { func didBecomeActive() { if let rootViewController = rootViewController { - ProfileExpirationAlerter.alertIfNeeded(viewControllerToPresentFrom: rootViewController) + AppExpirationAlerter.alertIfNeeded(viewControllerToPresentFrom: rootViewController) } settingsManager?.didBecomeActive() deviceDataManager?.didBecomeActive() @@ -603,7 +601,7 @@ extension LoopAppManager: TemporaryScheduleOverrideHistoryDelegate { func temporaryScheduleOverrideHistoryDidUpdate(_ history: TemporaryScheduleOverrideHistory) { UserDefaults.appGroup?.overrideHistory = history - deviceDataManager.remoteDataServicesManager.temporaryScheduleOverrideHistoryDidUpdate() + deviceDataManager.remoteDataServicesManager.triggerUpload(for: .overrides) } } diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index a97944e9ea..2319f4eceb 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -364,7 +364,7 @@ final class LoopDataManager { private var retrospectiveGlucoseDiscrepancies: [GlucoseEffect]? { didSet { - retrospectiveGlucoseDiscrepanciesSummed = retrospectiveGlucoseDiscrepancies?.combinedSums(of: LoopConstants.retrospectiveCorrectionGroupingInterval * retrospectiveCorrectionGroupingIntervalMultiplier) + retrospectiveGlucoseDiscrepanciesSummed = retrospectiveGlucoseDiscrepancies?.combinedSums(of: LoopMath.retrospectiveCorrectionGroupingInterval * retrospectiveCorrectionGroupingIntervalMultiplier) } } @@ -441,9 +441,9 @@ final class LoopDataManager { if lastIntegralRetrospectiveCorrectionEnabled != currentIntegralRetrospectiveCorrectionEnabled || cachedRetrospectiveCorrection == nil { lastIntegralRetrospectiveCorrectionEnabled = currentIntegralRetrospectiveCorrectionEnabled if currentIntegralRetrospectiveCorrectionEnabled { - cachedRetrospectiveCorrection = IntegralRetrospectiveCorrection(effectDuration: LoopSettings.retrospectiveCorrectionEffectDuration) + cachedRetrospectiveCorrection = IntegralRetrospectiveCorrection(effectDuration: LoopMath.retrospectiveCorrectionEffectDuration) } else { - cachedRetrospectiveCorrection = StandardRetrospectiveCorrection(effectDuration: LoopSettings.retrospectiveCorrectionEffectDuration) + cachedRetrospectiveCorrection = StandardRetrospectiveCorrection(effectDuration: LoopMath.retrospectiveCorrectionEffectDuration) } } @@ -741,26 +741,6 @@ extension LoopDataManager { } } - - /// Adds and stores new pump events - /// - /// - Parameters: - /// - events: The pump events to add - /// - completion: A closure called once upon completion - /// - lastReconciliation: The date that pump events were most recently reconciled against recorded pump history. Pump events are assumed to be reflective of delivery up until this point in time. If reservoir values are recorded after this time, they may be used to supplement event based delivery. - /// - error: An error explaining why the events could not be saved. - func addPumpEvents(_ events: [NewPumpEvent], lastReconciliation: Date?, completion: @escaping (_ error: DoseStore.DoseStoreError?) -> Void) { - doseStore.addPumpEvents(events, lastReconciliation: lastReconciliation) { (error) in - completion(error) - - self.dataAccessQueue.async { - if error == nil { - self.clearCachedInsulinEffects() - } - } - } - } - /// Logs a new external bolus insulin dose in the DoseStore and HealthKit /// /// - Parameters: @@ -910,8 +890,8 @@ extension LoopDataManager { notify(forChange: .loopFinished) if FeatureFlags.missedMealNotifications { - let carbEffectStart = now().addingTimeInterval(-MissedMealSettings.maxRecency) - carbStore.getGlucoseEffects(start: carbEffectStart, end: now(), effectVelocities: insulinCounteractionEffects) {[weak self] result in + let samplesStart = now().addingTimeInterval(-MissedMealSettings.maxRecency) + carbStore.getGlucoseEffects(start: samplesStart, end: now(), effectVelocities: insulinCounteractionEffects) {[weak self] result in guard let self = self, case .success((_, let carbEffects)) = result @@ -921,15 +901,28 @@ extension LoopDataManager { } return } - - self.mealDetectionManager.generateMissedMealNotificationIfNeeded( - insulinCounteractionEffects: self.insulinCounteractionEffects, - carbEffects: carbEffects, - pendingAutobolusUnits: self.recommendedAutomaticDose?.recommendation.bolusUnits, - bolusDurationEstimator: { [unowned self] bolusAmount in - return self.delegate?.loopDataManager(self, estimateBolusDuration: bolusAmount) + + glucoseStore.getGlucoseSamples(start: samplesStart, end: now()) {[weak self] result in + guard + let self = self, + case .success(let glucoseSamples) = result + else { + if case .failure(let error) = result { + self?.logger.error("Failed to fetch glucose samples to check for missed meal: %{public}@", String(describing: error)) + } + return } - ) + + self.mealDetectionManager.generateMissedMealNotificationIfNeeded( + glucoseSamples: glucoseSamples, + insulinCounteractionEffects: self.insulinCounteractionEffects, + carbEffects: carbEffects, + pendingAutobolusUnits: self.recommendedAutomaticDose?.recommendation.bolusUnits, + bolusDurationEstimator: { [unowned self] bolusAmount in + return self.delegate?.loopDataManager(self, estimateBolusDuration: bolusAmount) + } + ) + } } } @@ -1005,7 +998,7 @@ extension LoopDataManager { if glucoseMomentumEffect == nil { updateGroup.enter() - glucoseStore.getRecentMomentumEffect { (result) -> Void in + glucoseStore.getRecentMomentumEffect(for: now()) { (result) -> Void in switch result { case .failure(let error): self.logger.error("Failure getting recent momentum effect: %{public}@", String(describing: error)) @@ -1101,7 +1094,7 @@ extension LoopDataManager { switch error { case .noData: // when there is no data, carbs on board is set to 0 - self.carbsOnBoard = CarbValue(startDate: Date(), quantity: HKQuantity(unit: .gram(), doubleValue: 0)) + self.carbsOnBoard = CarbValue(startDate: Date(), value: 0) default: self.carbsOnBoard = nil warnings.append(.fetchDataWarning(.carbsOnBoard(error: error))) @@ -1149,7 +1142,7 @@ extension LoopDataManager { dosingDecision.glucoseTargetRangeSchedule = settings.effectiveGlucoseTargetRangeSchedule() // These will be updated by updatePredictedGlucoseAndRecommendedDose, if possible - dosingDecision.predictedGlucose = predictedGlucose + dosingDecision.predictedGlucose = predictedGlucoseIncludingPendingInsulin dosingDecision.automaticDoseRecommendation = recommendedAutomaticDose?.recommendation // If the glucose prediction hasn't changed, then nothing has changed, so just use pre-existing recommendations @@ -1594,7 +1587,7 @@ extension LoopDataManager { insulinSensitivity: insulinSensitivity, basalRate: basalRate, correctionRange: correctionRange, - retrospectiveCorrectionGroupingInterval: LoopConstants.retrospectiveCorrectionGroupingInterval + retrospectiveCorrectionGroupingInterval: LoopMath.retrospectiveCorrectionGroupingInterval ) } @@ -1605,7 +1598,7 @@ extension LoopDataManager { let correctionRange = settings.glucoseTargetRangeSchedule!.quantityRange(at: glucose.startDate) let retrospectiveGlucoseDiscrepancies = insulinCounteractionEffects.subtracting(carbEffects, withUniformInterval: carbStore.delta) - let retrospectiveGlucoseDiscrepanciesSummed = retrospectiveGlucoseDiscrepancies.combinedSums(of: LoopConstants.retrospectiveCorrectionGroupingInterval * retrospectiveCorrectionGroupingIntervalMultiplier) + let retrospectiveGlucoseDiscrepanciesSummed = retrospectiveGlucoseDiscrepancies.combinedSums(of: LoopMath.retrospectiveCorrectionGroupingInterval * retrospectiveCorrectionGroupingIntervalMultiplier) return retrospectiveCorrection.computeEffect( startingAt: glucose, retrospectiveGlucoseDiscrepanciesSummed: retrospectiveGlucoseDiscrepanciesSummed, @@ -1613,7 +1606,7 @@ extension LoopDataManager { insulinSensitivity: insulinSensitivity, basalRate: basalRate, correctionRange: correctionRange, - retrospectiveCorrectionGroupingInterval: LoopConstants.retrospectiveCorrectionGroupingInterval + retrospectiveCorrectionGroupingInterval: LoopMath.retrospectiveCorrectionGroupingInterval ) } diff --git a/Loop/Managers/Missed Meal Detection/MealDetectionManager.swift b/Loop/Managers/Missed Meal Detection/MealDetectionManager.swift index 0941f8bf6b..a3922a873a 100644 --- a/Loop/Managers/Missed Meal Detection/MealDetectionManager.swift +++ b/Loop/Managers/Missed Meal Detection/MealDetectionManager.swift @@ -66,13 +66,22 @@ class MealDetectionManager { } // MARK: Meal Detection - func hasMissedMeal(insulinCounteractionEffects: [GlucoseEffectVelocity], carbEffects: [GlucoseEffect], completion: @escaping (MissedMealStatus) -> Void) { + func hasMissedMeal(glucoseSamples: [some GlucoseSampleValue], insulinCounteractionEffects: [GlucoseEffectVelocity], carbEffects: [GlucoseEffect], completion: @escaping (MissedMealStatus) -> Void) { let delta = TimeInterval(minutes: 5) let intervalStart = currentDate(timeIntervalSinceNow: -MissedMealSettings.maxRecency) let intervalEnd = currentDate(timeIntervalSinceNow: -MissedMealSettings.minRecency) let now = self.currentDate - + + let filteredGlucoseValues = glucoseSamples.filter { intervalStart <= $0.startDate && $0.startDate <= now } + + /// Only try to detect if there's a missed meal if there are no calibration/user-entered BGs, + /// since these can cause large jumps + guard !filteredGlucoseValues.containsUserEntered() else { + completion(.noMissedMeal) + return + } + let filteredCarbEffects = carbEffects.filterDateRange(intervalStart, now) /// Compute how much of the ICE effect we can't explain via our entered carbs @@ -214,12 +223,13 @@ class MealDetectionManager { /// - pendingAutobolusUnits: any autobolus units that are still being delivered. Used to delay the missed meal notification to avoid notifying during an autobolus. /// - bolusDurationEstimator: estimator of bolus duration that takes the units of the bolus as an input. Used to delay the missed meal notification to avoid notifying during an autobolus. func generateMissedMealNotificationIfNeeded( + glucoseSamples: [some GlucoseSampleValue], insulinCounteractionEffects: [GlucoseEffectVelocity], carbEffects: [GlucoseEffect], pendingAutobolusUnits: Double? = nil, bolusDurationEstimator: @escaping (Double) -> TimeInterval? ) { - hasMissedMeal(insulinCounteractionEffects: insulinCounteractionEffects, carbEffects: carbEffects) {[weak self] status in + hasMissedMeal(glucoseSamples: glucoseSamples, insulinCounteractionEffects: insulinCounteractionEffects, carbEffects: carbEffects) {[weak self] status in self?.manageMealNotifications(for: status, pendingAutobolusUnits: pendingAutobolusUnits, bolusDurationEstimator: bolusDurationEstimator) } } @@ -295,3 +305,11 @@ class MealDetectionManager { completionHandler(report.joined(separator: "\n")) } } + +fileprivate extension BidirectionalCollection where Element: GlucoseSampleValue, Index == Int { + /// Returns whether there are any user-entered or calibration points + /// Runtime: O(n) + func containsUserEntered() -> Bool { + return containsCalibrations() || filter({ $0.wasUserEntered }).count != 0 + } +} diff --git a/Loop/Managers/OnboardingManager.swift b/Loop/Managers/OnboardingManager.swift index b39e0d7d35..b9f6c8c232 100644 --- a/Loop/Managers/OnboardingManager.swift +++ b/Loop/Managers/OnboardingManager.swift @@ -15,6 +15,7 @@ class OnboardingManager { private let pluginManager: PluginManager private let bluetoothProvider: BluetoothProvider private let deviceDataManager: DeviceDataManager + private let statefulPluginManager: StatefulPluginManager private let servicesManager: ServicesManager private let loopDataManager: LoopDataManager private let supportManager: SupportManager @@ -39,10 +40,20 @@ class OnboardingManager { private var onboardingCompletion: (() -> Void)? - init(pluginManager: PluginManager, bluetoothProvider: BluetoothProvider, deviceDataManager: DeviceDataManager, servicesManager: ServicesManager, loopDataManager: LoopDataManager, supportManager: SupportManager, windowProvider: WindowProvider?, userDefaults: UserDefaults = .standard) { + init(pluginManager: PluginManager, + bluetoothProvider: BluetoothProvider, + deviceDataManager: DeviceDataManager, + statefulPluginManager: StatefulPluginManager, + servicesManager: ServicesManager, + loopDataManager: LoopDataManager, + supportManager: SupportManager, + windowProvider: WindowProvider?, + userDefaults: UserDefaults = .standard) + { self.pluginManager = pluginManager self.bluetoothProvider = bluetoothProvider self.deviceDataManager = deviceDataManager + self.statefulPluginManager = statefulPluginManager self.servicesManager = servicesManager self.loopDataManager = loopDataManager self.supportManager = supportManager @@ -122,7 +133,7 @@ class OnboardingManager { let onboarding = onboardingType.createOnboarding() guard !onboarding.isOnboarded else { - completedOnboardingIdentifiers.append(onboarding.onboardingIdentifier) + completedOnboardingIdentifiers.append(onboarding.pluginIdentifier) continue } @@ -155,7 +166,7 @@ class OnboardingManager { dispatchPrecondition(condition: .onQueue(.main)) if let activeOnboarding = self.activeOnboarding, !isSuspended { - completedOnboardingIdentifiers.append(activeOnboarding.onboardingIdentifier) + completedOnboardingIdentifiers.append(activeOnboarding.pluginIdentifier) self.activeOnboarding = nil } continueOnboarding() @@ -238,25 +249,25 @@ class OnboardingManager { extension OnboardingManager: OnboardingDelegate { func onboardingDidUpdateState(_ onboarding: OnboardingUI) { - guard onboarding.onboardingIdentifier == activeOnboarding?.onboardingIdentifier else { return } + guard onboarding.pluginIdentifier == activeOnboarding?.pluginIdentifier else { return } userDefaults.onboardingManagerActiveOnboardingRawValue = onboarding.rawValue } func onboarding(_ onboarding: OnboardingUI, hasNewTherapySettings therapySettings: TherapySettings) { - guard onboarding.onboardingIdentifier == activeOnboarding?.onboardingIdentifier else { return } + guard onboarding.pluginIdentifier == activeOnboarding?.pluginIdentifier else { return } loopDataManager.therapySettings = therapySettings } func onboarding(_ onboarding: OnboardingUI, hasNewDosingEnabled dosingEnabled: Bool) { - guard onboarding.onboardingIdentifier == activeOnboarding?.onboardingIdentifier else { return } + guard onboarding.pluginIdentifier == activeOnboarding?.pluginIdentifier else { return } loopDataManager.mutateSettings { settings in settings.dosingEnabled = dosingEnabled } } func onboardingDidSuspend(_ onboarding: OnboardingUI) { - log.debug("OnboardingUI %@ did suspend", onboarding.onboardingIdentifier) - guard onboarding.onboardingIdentifier == activeOnboarding?.onboardingIdentifier else { return } + log.debug("OnboardingUI %@ did suspend", onboarding.pluginIdentifier) + guard onboarding.pluginIdentifier == activeOnboarding?.pluginIdentifier else { return } self.isSuspended = true } } @@ -270,7 +281,7 @@ extension OnboardingManager: CompletionDelegate { return } - self.log.debug("completionNotifyingDidComplete during activeOnboarding", activeOnboarding.onboardingIdentifier) + self.log.debug("completionNotifyingDidComplete during activeOnboarding", activeOnboarding.pluginIdentifier) // The `completionNotifyingDidComplete` callback can be called by an onboarding plugin to signal that the user is done with // the onboarding UI, like when pausing, so the onboarding UI can be dismissed. This doesn't necessarily mean that the @@ -340,7 +351,7 @@ extension OnboardingManager: CGMManagerProvider { guard let cgmManager = deviceDataManager.cgmManager else { return deviceDataManager.setupCGMManager(withIdentifier: identifier, prefersToSkipUserInteraction: prefersToSkipUserInteraction) } - guard cgmManager.managerIdentifier == identifier else { + guard cgmManager.pluginIdentifier == identifier else { return .failure(OnboardingError.invalidState) } @@ -384,7 +395,7 @@ extension OnboardingManager: PumpManagerProvider { guard let pumpManager = deviceDataManager.pumpManager else { return deviceDataManager.setupPumpManager(withIdentifier: identifier, initialSettings: settings, prefersToSkipUserInteraction: prefersToSkipUserInteraction) } - guard pumpManager.managerIdentifier == identifier else { + guard pumpManager.pluginIdentifier == identifier else { return .failure(OnboardingError.invalidState) } @@ -396,15 +407,22 @@ extension OnboardingManager: PumpManagerProvider { } } +// MARK: - StatefulPluggableProvider + +extension OnboardingManager: StatefulPluggableProvider { + func statefulPlugin(withIdentifier identifier: String) -> StatefulPluggable? { + statefulPluginManager.statefulPlugin(withIdentifier: identifier) } +} + // MARK: - ServiceProvider -extension OnboardingManager: ServiceProvider { +extension OnboardingManager: ServiceProvider { var activeServices: [Service] { servicesManager.activeServices } var availableServices: [ServiceDescriptor] { servicesManager.availableServices } func onboardService(withIdentifier identifier: String) -> Swift.Result, Error> { - guard let service = activeServices.first(where: { $0.serviceIdentifier == identifier }) else { + guard let service = activeServices.first(where: { $0.pluginIdentifier == identifier }) else { return servicesManager.setupService(withIdentifier: identifier) } @@ -421,6 +439,7 @@ extension OnboardingManager: ServiceProvider { } // MARK: - TherapySettingsProvider + extension OnboardingManager: TherapySettingsProvider { var onboardingTherapySettings: TherapySettings { return loopDataManager.therapySettings @@ -446,7 +465,7 @@ fileprivate extension OnboardingUI { var rawValue: RawValue { return [ - "onboardingIdentifier": onboardingIdentifier, + "onboardingIdentifier": pluginIdentifier, "state": rawState ] } diff --git a/Loop/Managers/ProfileExpirationAlerter.swift b/Loop/Managers/ProfileExpirationAlerter.swift deleted file mode 100644 index 3aa742732b..0000000000 --- a/Loop/Managers/ProfileExpirationAlerter.swift +++ /dev/null @@ -1,86 +0,0 @@ -// -// ProfileExpirationAlerter.swift -// Loop -// -// Created by Pete Schwamb on 8/21/21. -// Copyright © 2021 LoopKit Authors. All rights reserved. -// - -import Foundation -import UserNotifications -import LoopCore - - -class ProfileExpirationAlerter { - - static let expirationAlertWindow: TimeInterval = .days(20) - static let settingsPageExpirationWarningModeWindow: TimeInterval = .days(3) - - static func alertIfNeeded(viewControllerToPresentFrom: UIViewController) { - - let now = Date() - - guard let profileExpiration = BuildDetails.default.profileExpiration, now > profileExpiration - expirationAlertWindow else { - return - } - - let timeUntilExpiration = profileExpiration.timeIntervalSince(now) - - let minimumTimeBetweenAlerts: TimeInterval = timeUntilExpiration > .hours(24) ? .days(2) : .hours(1) - - if let lastAlertDate = UserDefaults.appGroup?.lastProfileExpirationAlertDate { - guard now > lastAlertDate + minimumTimeBetweenAlerts else { - return - } - } - - let formatter = DateComponentsFormatter() - formatter.allowedUnits = [.day, .hour] - formatter.unitsStyle = .full - formatter.zeroFormattingBehavior = .dropLeading - formatter.maximumUnitCount = 1 - let timeUntilExpirationStr = formatter.string(from: timeUntilExpiration) - - let alertMessage = createVerboseAlertMessage(timeUntilExpirationStr: timeUntilExpirationStr!) - - let dialog = UIAlertController( - title: NSLocalizedString("Profile Expires Soon", comment: "The title for notification of upcoming profile expiration"), - message: alertMessage, - preferredStyle: .alert) - dialog.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Text for ok action on notification of upcoming profile expiration"), style: .default, handler: nil)) - dialog.addAction(UIAlertAction(title: NSLocalizedString("More Info", comment: "Text for more info action on notification of upcoming profile expiration"), style: .default, handler: { (_) in - UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/build/updating/")!) - })) - viewControllerToPresentFrom.present(dialog, animated: true, completion: nil) - - UserDefaults.appGroup?.lastProfileExpirationAlertDate = now - } - - static func createVerboseAlertMessage(timeUntilExpirationStr:String) -> String { - return String(format: NSLocalizedString("%1$@ will stop working in %2$@. You will need to update before that, with a new provisioning profile.", comment: "Format string for body for notification of upcoming provisioning profile expiration. (1: app name) (2: amount of time until expiration"), Bundle.main.bundleDisplayName, timeUntilExpirationStr) - } - - static func isNearProfileExpiration(profileExpiration:Date) -> Bool { - return profileExpiration.timeIntervalSinceNow < settingsPageExpirationWarningModeWindow - } - - static func createProfileExpirationSettingsMessage(profileExpiration:Date) -> String { - let nearExpiration = isNearProfileExpiration(profileExpiration: profileExpiration) - let maxUnitCount = nearExpiration ? 2 : 1 // only include hours in the msg if near expiration - let readableRelativeTime: String? = relativeTimeFormatter(maxUnitCount: maxUnitCount).string(from: profileExpiration.timeIntervalSinceNow) - let relativeTimeRemaining: String = readableRelativeTime ?? NSLocalizedString("Unknown time", comment: "Unknown amount of time in settings' profile expiration section") - let verboseMessage = createVerboseAlertMessage(timeUntilExpirationStr: relativeTimeRemaining) - let conciseMessage = relativeTimeRemaining + NSLocalizedString(" remaining", comment: "remaining time in setting's profile expiration section") - return nearExpiration ? verboseMessage : conciseMessage - } - - private static func relativeTimeFormatter(maxUnitCount:Int) -> DateComponentsFormatter { - let formatter = DateComponentsFormatter() - let includeHours = maxUnitCount == 2 - formatter.allowedUnits = includeHours ? [.day, .hour] : [.day] - formatter.unitsStyle = .full - formatter.zeroFormattingBehavior = .dropLeading - formatter.maximumUnitCount = maxUnitCount - return formatter; - } -} diff --git a/Loop/Managers/RemoteDataServicesManager.swift b/Loop/Managers/RemoteDataServicesManager.swift index 14a3416900..bf21376bc3 100644 --- a/Loop/Managers/RemoteDataServicesManager.swift +++ b/Loop/Managers/RemoteDataServicesManager.swift @@ -10,13 +10,14 @@ import os.log import Foundation import LoopKit -enum RemoteDataType: String { +enum RemoteDataType: String, CaseIterable { case alert = "Alert" case carb = "Carb" case dose = "Dose" case dosingDecision = "DosingDecision" case glucose = "Glucose" case pumpEvent = "PumpEvent" + case cgmEvent = "CgmEvent" case settings = "Settings" case overrides = "Overrides" @@ -64,7 +65,7 @@ final class RemoteDataServicesManager { func removeService(_ remoteDataService: RemoteDataService) { lock.withLock { - unlockedRemoteDataServices.removeAll { $0.serviceIdentifier == remoteDataService.serviceIdentifier } + unlockedRemoteDataServices.removeAll { $0.pluginIdentifier == remoteDataService.pluginIdentifier } } clearQueryAnchors(for: remoteDataService) } @@ -80,7 +81,7 @@ final class RemoteDataServicesManager { private func dispatchQueue(for remoteDataService: RemoteDataService, withRemoteDataType remoteDataType: RemoteDataType) -> DispatchQueue { - let key = UploadTaskKey(serviceIdentifier: remoteDataService.serviceIdentifier, remoteDataType: remoteDataType) + let key = UploadTaskKey(serviceIdentifier: remoteDataService.pluginIdentifier, remoteDataType: remoteDataType) return dispatchQueue(key) } @@ -129,6 +130,8 @@ final class RemoteDataServicesManager { private let glucoseStore: GlucoseStore + private let cgmEventStore: CgmEventStore + private let insulinDeliveryStore: InsulinDeliveryStore private let settingsStore: SettingsStore @@ -141,6 +144,7 @@ final class RemoteDataServicesManager { doseStore: DoseStore, dosingDecisionStore: DosingDecisionStore, glucoseStore: GlucoseStore, + cgmEventStore: CgmEventStore, settingsStore: SettingsStore, overrideHistory: TemporaryScheduleOverrideHistory, insulinDeliveryStore: InsulinDeliveryStore @@ -150,6 +154,7 @@ final class RemoteDataServicesManager { self.doseStore = doseStore self.dosingDecisionStore = dosingDecisionStore self.glucoseStore = glucoseStore + self.cgmEventStore = cgmEventStore self.insulinDeliveryStore = insulinDeliveryStore self.settingsStore = settingsStore self.overrideHistory = overrideHistory @@ -167,13 +172,11 @@ final class RemoteDataServicesManager { } private func clearQueryAnchors(for remoteDataService: RemoteDataService) { - clearAlertQueryAnchor(for: remoteDataService) - clearCarbQueryAnchor(for: remoteDataService) - clearDoseQueryAnchor(for: remoteDataService) - clearDosingDecisionQueryAnchor(for: remoteDataService) - clearGlucoseQueryAnchor(for: remoteDataService) - clearPumpEventQueryAnchor(for: remoteDataService) - clearSettingsQueryAnchor(for: remoteDataService) + for remoteDataType in RemoteDataType.allCases { + dispatchQueue(for: remoteDataService, withRemoteDataType: remoteDataType).async { + UserDefaults.appGroup?.deleteQueryAnchor(for: remoteDataService, withRemoteDataType: remoteDataType) + } + } } func triggerUpload(for triggeringType: RemoteDataType) { @@ -195,6 +198,8 @@ final class RemoteDataServicesManager { remoteDataServices.forEach { self.uploadGlucoseData(to: $0) } case .pumpEvent: remoteDataServices.forEach { self.uploadPumpEventData(to: $0) } + case .cgmEvent: + remoteDataServices.forEach { self.uploadCgmEventData(to: $0) } case .settings: remoteDataServices.forEach { self.uploadSettingsData(to: $0) } case .overrides: @@ -220,15 +225,10 @@ final class RemoteDataServicesManager { } extension RemoteDataServicesManager { - - public func alertStoreHasUpdatedAlertData(_ alertStore: AlertStore) { - triggerUpload(for: .alert) - } - private func uploadAlertData(to remoteDataService: RemoteDataService) { uploadGroup.enter() - let key = UploadTaskKey(serviceIdentifier: remoteDataService.serviceIdentifier, remoteDataType: .alert) + let key = UploadTaskKey(serviceIdentifier: remoteDataService.pluginIdentifier, remoteDataType: .alert) dispatchQueue(key).async { let semaphore = DispatchSemaphore(value: 0) @@ -257,25 +257,13 @@ extension RemoteDataServicesManager { self.uploadGroup.leave() } } - - private func clearAlertQueryAnchor(for remoteDataService: RemoteDataService) { - dispatchQueue(for: remoteDataService, withRemoteDataType: .alert).async { - UserDefaults.appGroup?.deleteQueryAnchor(for: remoteDataService, withRemoteDataType: .alert) - } - } - } extension RemoteDataServicesManager { - - public func carbStoreHasUpdatedCarbData(_ carbStore: CarbStore) { - triggerUpload(for: .carb) - } - private func uploadCarbData(to remoteDataService: RemoteDataService) { uploadGroup.enter() - let key = UploadTaskKey(serviceIdentifier: remoteDataService.serviceIdentifier, remoteDataType: .carb) + let key = UploadTaskKey(serviceIdentifier: remoteDataService.pluginIdentifier, remoteDataType: .carb) dispatchQueue(key).async { let semaphore = DispatchSemaphore(value: 0) @@ -311,25 +299,13 @@ extension RemoteDataServicesManager { } } } - - private func clearCarbQueryAnchor(for remoteDataService: RemoteDataService) { - dispatchQueue(for: remoteDataService, withRemoteDataType: .carb).async { - UserDefaults.appGroup?.deleteQueryAnchor(for: remoteDataService, withRemoteDataType: .carb) - } - } - } extension RemoteDataServicesManager { - - public func insulinDeliveryStoreHasUpdatedDoseData(_ insulinDeliveryStore: InsulinDeliveryStore) { - triggerUpload(for: .dose) - } - private func uploadDoseData(to remoteDataService: RemoteDataService) { uploadGroup.enter() - let key = UploadTaskKey(serviceIdentifier: remoteDataService.serviceIdentifier, remoteDataType: .dose) + let key = UploadTaskKey(serviceIdentifier: remoteDataService.pluginIdentifier, remoteDataType: .dose) dispatchQueue(key).async { let semaphore = DispatchSemaphore(value: 0) @@ -365,25 +341,13 @@ extension RemoteDataServicesManager { } } } - - private func clearDoseQueryAnchor(for remoteDataService: RemoteDataService) { - dispatchQueue(for: remoteDataService, withRemoteDataType: .dose).async { - UserDefaults.appGroup?.deleteQueryAnchor(for: remoteDataService, withRemoteDataType: .dose) - } - } - } extension RemoteDataServicesManager { - - public func dosingDecisionStoreHasUpdatedDosingDecisionData(_ dosingDecisionStore: DosingDecisionStore) { - triggerUpload(for: .dosingDecision) - } - private func uploadDosingDecisionData(to remoteDataService: RemoteDataService) { uploadGroup.enter() - let key = UploadTaskKey(serviceIdentifier: remoteDataService.serviceIdentifier, remoteDataType: .dosingDecision) + let key = UploadTaskKey(serviceIdentifier: remoteDataService.pluginIdentifier, remoteDataType: .dosingDecision) dispatchQueue(key).async { let semaphore = DispatchSemaphore(value: 0) @@ -419,21 +383,9 @@ extension RemoteDataServicesManager { } } } - - private func clearDosingDecisionQueryAnchor(for remoteDataService: RemoteDataService) { - dispatchQueue(for: remoteDataService, withRemoteDataType: .dosingDecision).async { - UserDefaults.appGroup?.deleteQueryAnchor(for: remoteDataService, withRemoteDataType: .dosingDecision) - } - } - } extension RemoteDataServicesManager { - - public func glucoseStoreHasUpdatedGlucoseData(_ glucoseStore: GlucoseStore) { - triggerUpload(for: .glucose) - } - private func uploadGlucoseData(to remoteDataService: RemoteDataService) { if delegate?.shouldSyncToRemoteService == false { @@ -442,7 +394,7 @@ extension RemoteDataServicesManager { uploadGroup.enter() - let key = UploadTaskKey(serviceIdentifier: remoteDataService.serviceIdentifier, remoteDataType: .glucose) + let key = UploadTaskKey(serviceIdentifier: remoteDataService.pluginIdentifier, remoteDataType: .glucose) dispatchQueue(key).async { let semaphore = DispatchSemaphore(value: 0) @@ -478,25 +430,13 @@ extension RemoteDataServicesManager { } } } - - private func clearGlucoseQueryAnchor(for remoteDataService: RemoteDataService) { - dispatchQueue(for: remoteDataService, withRemoteDataType: .glucose).async { - UserDefaults.appGroup?.deleteQueryAnchor(for: remoteDataService, withRemoteDataType: .glucose) - } - } - } extension RemoteDataServicesManager { - - public func doseStoreHasUpdatedPumpEventData(_ doseStore: DoseStore) { - triggerUpload(for: .pumpEvent) - } - private func uploadPumpEventData(to remoteDataService: RemoteDataService) { uploadGroup.enter() - let key = UploadTaskKey(serviceIdentifier: remoteDataService.serviceIdentifier, remoteDataType: .pumpEvent) + let key = UploadTaskKey(serviceIdentifier: remoteDataService.pluginIdentifier, remoteDataType: .pumpEvent) dispatchQueue(for: remoteDataService, withRemoteDataType: .pumpEvent).async { let semaphore = DispatchSemaphore(value: 0) @@ -532,25 +472,13 @@ extension RemoteDataServicesManager { } } } - - private func clearPumpEventQueryAnchor(for remoteDataService: RemoteDataService) { - dispatchQueue(for: remoteDataService, withRemoteDataType: .pumpEvent).async { - UserDefaults.appGroup?.deleteQueryAnchor(for: remoteDataService, withRemoteDataType: .pumpEvent) - } - } - } extension RemoteDataServicesManager { - - public func settingsStoreHasUpdatedSettingsData(_ settingsStore: SettingsStore) { - triggerUpload(for: .settings) - } - private func uploadSettingsData(to remoteDataService: RemoteDataService) { uploadGroup.enter() - let key = UploadTaskKey(serviceIdentifier: remoteDataService.serviceIdentifier, remoteDataType: .settings) + let key = UploadTaskKey(serviceIdentifier: remoteDataService.pluginIdentifier, remoteDataType: .settings) dispatchQueue(for: remoteDataService, withRemoteDataType: .settings).async { let semaphore = DispatchSemaphore(value: 0) @@ -586,25 +514,13 @@ extension RemoteDataServicesManager { } } } - - private func clearSettingsQueryAnchor(for remoteDataService: RemoteDataService) { - dispatchQueue(for: remoteDataService, withRemoteDataType: .settings).async { - UserDefaults.appGroup?.deleteQueryAnchor(for: remoteDataService, withRemoteDataType: .settings) - } - } - } extension RemoteDataServicesManager { - - public func temporaryScheduleOverrideHistoryDidUpdate() { - triggerUpload(for: .overrides) - } - private func uploadTemporaryOverrideData(to remoteDataService: RemoteDataService) { uploadGroup.enter() - let key = UploadTaskKey(serviceIdentifier: remoteDataService.serviceIdentifier, remoteDataType: .overrides) + let key = UploadTaskKey(serviceIdentifier: remoteDataService.pluginIdentifier, remoteDataType: .overrides) dispatchQueue(for: remoteDataService, withRemoteDataType: .overrides).async { let semaphore = DispatchSemaphore(value: 0) @@ -629,10 +545,46 @@ extension RemoteDataServicesManager { self.uploadGroup.leave() } } +} - private func clearTemporaryOverrideQueryAnchor(for remoteDataService: RemoteDataService) { - dispatchQueue(for: remoteDataService, withRemoteDataType: .overrides).async { - UserDefaults.appGroup?.deleteQueryAnchor(for: remoteDataService, withRemoteDataType: .overrides) +extension RemoteDataServicesManager { + private func uploadCgmEventData(to remoteDataService: RemoteDataService) { + uploadGroup.enter() + + let key = UploadTaskKey(serviceIdentifier: remoteDataService.pluginIdentifier, remoteDataType: .pumpEvent) + + dispatchQueue(for: remoteDataService, withRemoteDataType: .cgmEvent).async { + let semaphore = DispatchSemaphore(value: 0) + let previousQueryAnchor = UserDefaults.appGroup?.getQueryAnchor(for: remoteDataService, withRemoteDataType: .cgmEvent) ?? CgmEventStore.QueryAnchor() + var continueUpload = false + + self.cgmEventStore.executeCgmEventQuery(fromQueryAnchor: previousQueryAnchor) { result in + switch result { + case .failure(let error): + self.log.error("Error querying cgm event data: %{public}@", String(describing: error)) + semaphore.signal() + case .success(let queryAnchor, let data): + remoteDataService.uploadCgmEventData(data) { result in + switch result { + case .failure(let error): + self.log.error("Error synchronizing cgm event data: %{public}@", String(describing: error)) + self.uploadFailed(key) + case .success: + UserDefaults.appGroup?.setQueryAnchor(for: remoteDataService, withRemoteDataType: .cgmEvent, queryAnchor) + continueUpload = queryAnchor != previousQueryAnchor + self.uploadSucceeded(key) + } + semaphore.signal() + } + } + } + + semaphore.wait() + self.uploadGroup.leave() + + if continueUpload { + self.uploadPumpEventData(to: remoteDataService) + } } } } @@ -648,7 +600,7 @@ extension RemoteDataServicesManager { func serviceForPushNotification(_ notification: [String: AnyObject]) throws -> RemoteDataService { let defaultServiceIdentifier = "NightscoutService" let serviceIdentifier = notification["serviceIdentifier"] as? String ?? defaultServiceIdentifier - guard let service = remoteDataServices.first(where: {$0.serviceIdentifier == serviceIdentifier}) else { + guard let service = remoteDataServices.first(where: {$0.pluginIdentifier == serviceIdentifier}) else { throw RemoteDataServicesManagerCommandError.unsupportedServiceIdentifier(serviceIdentifier) } return service @@ -674,7 +626,7 @@ protocol RemoteDataServicesManagerDelegate: AnyObject { fileprivate extension UserDefaults { private func queryAnchorKey(for remoteDataService: RemoteDataService, withRemoteDataType remoteDataType: RemoteDataType) -> String { - return "com.loopkit.Loop.RemoteDataServicesManager.\(remoteDataService.serviceIdentifier).\(remoteDataType.rawValue)QueryAnchor" + return "com.loopkit.Loop.RemoteDataServicesManager.\(remoteDataService.pluginIdentifier).\(remoteDataType.rawValue)QueryAnchor" } func getQueryAnchor(for remoteDataService: RemoteDataService, withRemoteDataType remoteDataType: RemoteDataType) -> T? where T: RawRepresentable, T.RawValue == [String: Any] { diff --git a/Loop/Managers/Service.swift b/Loop/Managers/Service.swift index 3966109931..9f4b2f0eee 100644 --- a/Loop/Managers/Service.swift +++ b/Loop/Managers/Service.swift @@ -13,11 +13,11 @@ import MockKit let staticServices: [Service.Type] = [MockService.self] let staticServicesByIdentifier: [String: Service.Type] = staticServices.reduce(into: [:]) { (map, Type) in - map[Type.serviceIdentifier] = Type + map[Type.pluginIdentifier] = Type } let availableStaticServices = staticServices.map { (Type) -> ServiceDescriptor in - return ServiceDescriptor(identifier: Type.serviceIdentifier, localizedTitle: Type.localizedTitle) + return ServiceDescriptor(identifier: Type.pluginIdentifier, localizedTitle: Type.localizedTitle) } func ServiceFromRawValue(_ rawValue: [String: Any]) -> Service? { @@ -30,16 +30,3 @@ func ServiceFromRawValue(_ rawValue: [String: Any]) -> Service? { return ServiceType.init(rawState: rawState) } - -extension Service { - - typealias RawValue = [String: Any] - - var rawValue: RawValue { - return [ - "serviceIdentifier": serviceIdentifier, - "state": rawState - ] - } - -} diff --git a/Loop/Managers/ServicesManager.swift b/Loop/Managers/ServicesManager.swift index 2593560706..2393ceb073 100644 --- a/Loop/Managers/ServicesManager.swift +++ b/Loop/Managers/ServicesManager.swift @@ -124,6 +124,7 @@ class ServicesManager { public func addActiveService(_ service: Service) { servicesLock.withLock { service.serviceDelegate = self + service.stateDelegate = self services.append(service) @@ -153,9 +154,10 @@ class ServicesManager { analyticsServicesManager.removeService(analyticsService) } - services.removeAll { $0.serviceIdentifier == service.serviceIdentifier } + services.removeAll { $0.pluginIdentifier == service.pluginIdentifier } service.serviceDelegate = nil + service.stateDelegate = nil saveState() } @@ -171,6 +173,7 @@ class ServicesManager { rawServices.forEach { rawValue in if let service = serviceFromRawValue(rawValue) { service.serviceDelegate = self + service.stateDelegate = self services.append(service) @@ -238,6 +241,19 @@ public protocol ServicesManagerDelegate: AnyObject { func deliverCarbs(amountInGrams: Double, absorptionTime: TimeInterval?, foodType: String?, startDate: Date?) async throws } +// MARK: - StatefulPluggableDelegate +extension ServicesManager: StatefulPluggableDelegate { + func pluginDidUpdateState(_ plugin: StatefulPluggable) { + saveState() + } + + func pluginWantsDeletion(_ plugin: StatefulPluggable) { + guard let service = plugin as? Service else { return } + log.default("Service with identifier '%{public}@' deleted", service.pluginIdentifier) + removeActiveService(service) + } +} + // MARK: - ServiceDelegate extension ServicesManager: ServiceDelegate { @@ -256,15 +272,6 @@ extension ServicesManager: ServiceDelegate { return semanticVersion } - - func serviceDidUpdateState(_ service: Service) { - saveState() - } - - func serviceWantsDeletion(_ service: Service) { - log.default("Service with identifier '%{public}@' deleted", service.serviceIdentifier) - removeActiveService(service) - } func enactRemoteOverride(name: String, durationTime: TimeInterval?, remoteAddress: String) async throws { @@ -380,16 +387,28 @@ extension ServicesManager: AlertIssuer { extension ServicesManager: ServiceOnboardingDelegate { func serviceOnboarding(didCreateService service: Service) { - log.default("Service with identifier '%{public}@' created", service.serviceIdentifier) + log.default("Service with identifier '%{public}@' created", service.pluginIdentifier) addActiveService(service) } func serviceOnboarding(didOnboardService service: Service) { precondition(service.isOnboarded) - log.default("Service with identifier '%{public}@' onboarded", service.serviceIdentifier) + log.default("Service with identifier '%{public}@' onboarded", service.pluginIdentifier) } } extension ServicesManager { var availableSupports: [SupportUI] { activeServices.compactMap { $0 as? SupportUI } } } + +// Service extension for rawValue +extension Service { + typealias RawValue = [String: Any] + + var rawValue: RawValue { + return [ + "serviceIdentifier": pluginIdentifier, + "state": rawState + ] + } +} diff --git a/Loop/Managers/SettingsManager.swift b/Loop/Managers/SettingsManager.swift index f7421b97f7..e3fdb60bf7 100644 --- a/Loop/Managers/SettingsManager.swift +++ b/Loop/Managers/SettingsManager.swift @@ -211,7 +211,7 @@ class SettingsManager { // MARK: - SettingsStoreDelegate extension SettingsManager: SettingsStoreDelegate { func settingsStoreHasUpdatedSettingsData(_ settingsStore: SettingsStore) { - remoteDataServicesManager?.settingsStoreHasUpdatedSettingsData(settingsStore) + remoteDataServicesManager?.triggerUpload(for: .settings) } } diff --git a/Loop/Managers/StatefulPluggable.swift b/Loop/Managers/StatefulPluggable.swift new file mode 100644 index 0000000000..ab1be4754d --- /dev/null +++ b/Loop/Managers/StatefulPluggable.swift @@ -0,0 +1,20 @@ +// +// StatefulPluggable.swift +// Loop +// +// Created by Nathaniel Hamming on 2023-09-13. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import LoopKit + +extension StatefulPluggable { + typealias RawValue = [String: Any] + + var rawValue: RawValue { + return [ + "statefulPluginIdentifier": pluginIdentifier, + "state": rawState + ] + } +} diff --git a/Loop/Managers/StatefulPluginManager.swift b/Loop/Managers/StatefulPluginManager.swift new file mode 100644 index 0000000000..22fc035b0c --- /dev/null +++ b/Loop/Managers/StatefulPluginManager.swift @@ -0,0 +1,125 @@ +// +// StatefulPluginManager.swift +// Loop +// +// Created by Nathaniel Hamming on 2023-09-06. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import LoopKit +import LoopKitUI +import LoopCore +import Combine + +class StatefulPluginManager: StatefulPluggableProvider { + + private let pluginManager: PluginManager + + private let servicesManager: ServicesManager + + private var statefulPlugins = [StatefulPluggable]() + + private let statefulPluginLock = UnfairLock() + + @PersistedProperty(key: "StatefulPlugins") + var rawStatefulPlugins: [StatefulPluggable.RawStateValue]? + + init(pluginManager: PluginManager, + servicesManager: ServicesManager) + { + self.pluginManager = pluginManager + self.servicesManager = servicesManager + restoreState() + } + + public var availableStatefulPluginIdentifiers: [String] { + return pluginManager.availableStatefulPluginIdentifiers + } + + func statefulPlugin(withIdentifier identifier: String) -> StatefulPluggable? { + for plugin in statefulPlugins { + if plugin.pluginIdentifier == identifier { + return plugin + } + } + + return setupStatefulPlugin(withIdentifier: identifier) + } + + func statefulPluginType(withIdentifier identifier: String) -> StatefulPluggable.Type? { + pluginManager.getStatefulPluginTypeByIdentifier(identifier) + } + + func setupStatefulPlugin(withIdentifier identifier: String) -> StatefulPluggable? { + guard let statefulPluinType = pluginManager.getStatefulPluginTypeByIdentifier(identifier) else { return nil } + + // init without raw value + let statefulPlugin = statefulPluinType.init(rawState: [:]) + statefulPlugin?.initializationComplete(for: servicesManager.activeServices) + addActiveStatefulPlugin(statefulPlugin) + + return statefulPlugin + } + + private func statefulPluginTypeFromRawValue(_ rawValue: StatefulPluggable.RawStateValue) -> StatefulPluggable.Type? { + guard let identifier = rawValue["statefulPluginIdentifier"] as? String else { + return nil + } + + return statefulPluginType(withIdentifier: identifier) + } + + private func statefulPluginFromRawValue(_ rawValue: StatefulPluggable.RawStateValue) -> StatefulPluggable? { + guard let statefulPluginType = statefulPluginTypeFromRawValue(rawValue), + let rawState = rawValue["state"] as? StatefulPluggable.RawStateValue + else { + return nil + } + + return statefulPluginType.init(rawState: rawState) + } + + public var activeStatefulPlugins: [StatefulPluggable] { + return statefulPluginLock.withLock { statefulPlugins } + } + + public func addActiveStatefulPlugin(_ statefulPlugin: StatefulPluggable?) { + guard let statefulPlugin = statefulPlugin else { return } + statefulPluginLock.withLock { + statefulPlugin.stateDelegate = self + statefulPlugins.append(statefulPlugin) + saveState() + } + } + + public func removeActiveStatefulPlugin(_ statefulPlugin: StatefulPluggable) { + statefulPluginLock.withLock { + statefulPlugins.removeAll { $0.pluginIdentifier == statefulPlugin.pluginIdentifier } + saveState() + } + } + + private func saveState() { + rawStatefulPlugins = statefulPlugins.compactMap { $0.rawValue } + } + + private func restoreState() { + let rawStatefulPlugins = rawStatefulPlugins ?? [] + rawStatefulPlugins.forEach { rawValue in + if let statefulPlugin = statefulPluginFromRawValue(rawValue) { + statefulPlugin.initializationComplete(for: servicesManager.activeServices) + statefulPlugins.append(statefulPlugin) + } + } + } +} + +extension StatefulPluginManager: StatefulPluggableDelegate { + func pluginDidUpdateState(_ plugin: StatefulPluggable) { + saveState() + } + + func pluginWantsDeletion(_ plugin: LoopKit.StatefulPluggable) { + removeActiveStatefulPlugin(plugin) + } +} diff --git a/Loop/Managers/Store Protocols/DoseStoreProtocol.swift b/Loop/Managers/Store Protocols/DoseStoreProtocol.swift index ebbd104da4..dd21ea2a1f 100644 --- a/Loop/Managers/Store Protocols/DoseStoreProtocol.swift +++ b/Loop/Managers/Store Protocols/DoseStoreProtocol.swift @@ -35,8 +35,8 @@ protocol DoseStoreProtocol: AnyObject { var pumpEventQueryAfterDate: Date { get } // MARK: dose management - func addPumpEvents(_ events: [NewPumpEvent], lastReconciliation: Date?, completion: @escaping (_ error: DoseStore.DoseStoreError?) -> Void) - + func addPumpEvents(_ events: [NewPumpEvent], lastReconciliation: Date?, replacePendingEvents: Bool, completion: @escaping (_ error: DoseStore.DoseStoreError?) -> Void) + func addReservoirValue(_ unitVolume: Double, at date: Date, completion: @escaping (_ value: ReservoirValue?, _ previousValue: ReservoirValue?, _ areStoredValuesContinuous: Bool, _ error: DoseStore.DoseStoreError?) -> Void) func getNormalizedDoseEntries(start: Date, end: Date?, completion: @escaping (_ result: DoseStoreResult<[DoseEntry]>) -> Void) diff --git a/Loop/Managers/Store Protocols/GlucoseStoreProtocol.swift b/Loop/Managers/Store Protocols/GlucoseStoreProtocol.swift index b549d9b0df..adde73c4c7 100644 --- a/Loop/Managers/Store Protocols/GlucoseStoreProtocol.swift +++ b/Loop/Managers/Store Protocols/GlucoseStoreProtocol.swift @@ -29,7 +29,7 @@ protocol GlucoseStoreProtocol: AnyObject { func executeGlucoseQuery(fromQueryAnchor queryAnchor: GlucoseStore.QueryAnchor?, limit: Int, completion: @escaping (GlucoseStore.GlucoseQueryResult) -> Void) // MARK: Effect Calculation - func getRecentMomentumEffect(_ completion: @escaping (_ result: Result<[GlucoseEffect], Error>) -> Void) + func getRecentMomentumEffect(for date: Date?, _ completion: @escaping (_ result: Result<[GlucoseEffect], Error>) -> Void) func getCounteractionEffects(start: Date, end: Date?, to effects: [GlucoseEffect], _ completion: @escaping (_ effects: Result<[GlucoseEffectVelocity], Error>) -> Void) diff --git a/Loop/Managers/SupportManager.swift b/Loop/Managers/SupportManager.swift index dae07c7e25..58cddddf74 100644 --- a/Loop/Managers/SupportManager.swift +++ b/Loop/Managers/SupportManager.swift @@ -54,7 +54,7 @@ public final class SupportManager { self.pluginManager = pluginManager self.staticSupportTypes = [] staticSupportTypesByIdentifier = self.staticSupportTypes.reduce(into: [:]) { (map, type) in - map[type.supportIdentifier] = type + map[type.pluginIdentifier] = type } restoreState() @@ -75,7 +75,7 @@ public final class SupportManager { for bundle in remainingSupportBundles { do { if let support = try bundle.loadAndInstantiateSupport() { - log.debug("Loaded support plugin: %{public}@", support.identifier) + log.debug("Loaded support plugin: %{public}@", support.pluginIdentifier) addSupport(support) } } catch { @@ -111,8 +111,8 @@ public final class SupportManager { extension SupportManager { func addSupport(_ support: SupportUI) { supports.mutate { - if $0[support.identifier] == nil { - $0[support.identifier] = support + if $0[support.pluginIdentifier] == nil { + $0[support.pluginIdentifier] = support support.delegate = self } } @@ -124,7 +124,7 @@ extension SupportManager { func removeSupport(_ support: SupportUI) { supports.mutate { - $0[support.identifier] = nil + $0[support.pluginIdentifier] = nil support.delegate = self } } @@ -156,7 +156,7 @@ extension SupportManager { supports.value.values.forEach { support in group.addTask { - return (await support.checkVersion(bundleIdentifier: Bundle.main.bundleIdentifier!, currentVersion: Bundle.main.shortVersionString), support.identifier) + return (await support.checkVersion(bundleIdentifier: Bundle.main.bundleIdentifier!, currentVersion: Bundle.main.shortVersionString), support.pluginIdentifier) } } @@ -331,7 +331,7 @@ fileprivate extension UserDefaults { extension SupportUI { var rawValue: RawStateValue { return [ - "supportIdentifier": Self.supportIdentifier, + "supportIdentifier": Self.pluginIdentifier, "state": rawState ] } diff --git a/Loop/Managers/TestingScenariosManager.swift b/Loop/Managers/TestingScenariosManager.swift index eabf1b060a..b71e357433 100644 --- a/Loop/Managers/TestingScenariosManager.swift +++ b/Loop/Managers/TestingScenariosManager.swift @@ -199,7 +199,7 @@ extension TestingScenariosManagerRequirements { if instance.hasCGMData { if let cgmManager = deviceManager.cgmManager as? TestingCGMManager { if instance.shouldReloadManager?.cgm == true { - testingCGMManager = reloadCGMManager(withIdentifier: cgmManager.managerIdentifier) + testingCGMManager = reloadCGMManager(withIdentifier: cgmManager.pluginIdentifier) } else { testingCGMManager = cgmManager } @@ -212,7 +212,7 @@ extension TestingScenariosManagerRequirements { if instance.hasPumpData { if let pumpManager = deviceManager.pumpManager as? TestingPumpManager { if instance.shouldReloadManager?.pump == true { - testingPumpManager = reloadPumpManager(withIdentifier: pumpManager.managerIdentifier) + testingPumpManager = reloadPumpManager(withIdentifier: pumpManager.pluginIdentifier) } else { testingPumpManager = pumpManager } @@ -243,9 +243,9 @@ extension TestingScenariosManagerRequirements { } instance.deviceActions.forEach { [testingCGMManager, testingPumpManager] action in - if testingCGMManager?.managerIdentifier == action.managerIdentifier { + if testingCGMManager?.pluginIdentifier == action.managerIdentifier { testingCGMManager?.trigger(action: action) - } else if testingPumpManager?.managerIdentifier == action.managerIdentifier { + } else if testingPumpManager?.pluginIdentifier == action.managerIdentifier { testingPumpManager?.trigger(action: action) } } diff --git a/Loop/Models/LoopConstants.swift b/Loop/Models/LoopConstants.swift index a62fc13849..fb69c8275f 100644 --- a/Loop/Models/LoopConstants.swift +++ b/Loop/Models/LoopConstants.swift @@ -52,9 +52,6 @@ enum LoopConstants { // Percentage of recommended dose to apply as bolus when using automatic bolus dosing strategy static let bolusPartialApplicationFactor = 0.4 - /// The interval over which to aggregate changes in glucose for retrospective correction - static let retrospectiveCorrectionGroupingInterval = TimeInterval(minutes: 30) - /// Loop completion aging category limits static let completionFreshLimit = TimeInterval(minutes: 6) static let completionAgingLimit = TimeInterval(minutes: 16) diff --git a/Loop/Models/LoopSettings+Loop.swift b/Loop/Models/LoopSettings+Loop.swift index fd35b6416b..e4952934cb 100644 --- a/Loop/Models/LoopSettings+Loop.swift +++ b/Loop/Models/LoopSettings+Loop.swift @@ -16,8 +16,5 @@ extension LoopSettings { inputs.remove(.retrospection) } return inputs - } - - static let retrospectiveCorrectionEffectDuration = TimeInterval(hours: 1) - + } } diff --git a/Loop/Plugins/PluginManager.swift b/Loop/Plugins/PluginManager.swift index bec19e0602..a254d26872 100644 --- a/Loop/Plugins/PluginManager.swift +++ b/Loop/Plugins/PluginManager.swift @@ -145,6 +145,37 @@ class PluginManager { return ServiceDescriptor(identifier: identifier, localizedTitle: title) }) } + + func getStatefulPluginTypeByIdentifier(_ identifier: String) -> StatefulPluggable.Type? { + for bundle in pluginBundles { + if let name = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.statefulPluginIdentifier.rawValue) as? String, name == identifier { + do { + try bundle.loadAndReturnError() + + if let principalClass = bundle.principalClass as? NSObject.Type { + + if let plugin = principalClass.init() as? StatefulPlugin { + return plugin.pluginType + } else { + fatalError("PrincipalClass does not conform to StatefulPlugin") + } + + } else { + fatalError("PrincipalClass not found") + } + } catch let error { + log.error("Error loading plugin: %{public}@", String(describing: error)) + } + } + } + return nil + } + + var availableStatefulPluginIdentifiers: [String] { + return pluginBundles.compactMap({ (bundle) -> String? in + return bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.statefulPluginIdentifier.rawValue) as? String + }) + } func getOnboardingTypeByIdentifier(_ identifier: String) -> OnboardingUI.Type? { for bundle in pluginBundles { @@ -201,18 +232,18 @@ class PluginManager { } return nil } - } extension Bundle { var isPumpManagerPlugin: Bool { object(forInfoDictionaryKey: LoopPluginBundleKey.pumpManagerIdentifier.rawValue) as? String != nil } var isCGMManagerPlugin: Bool { object(forInfoDictionaryKey: LoopPluginBundleKey.cgmManagerIdentifier.rawValue) as? String != nil } + var isStatefulPlugin: Bool { object(forInfoDictionaryKey: LoopPluginBundleKey.statefulPluginIdentifier.rawValue) as? String != nil } var isServicePlugin: Bool { object(forInfoDictionaryKey: LoopPluginBundleKey.serviceIdentifier.rawValue) as? String != nil } var isOnboardingPlugin: Bool { object(forInfoDictionaryKey: LoopPluginBundleKey.onboardingIdentifier.rawValue) as? String != nil } var isSupportPlugin: Bool { object(forInfoDictionaryKey: LoopPluginBundleKey.supportIdentifier.rawValue) as? String != nil } - var isLoopPlugin: Bool { isPumpManagerPlugin || isCGMManagerPlugin || isServicePlugin || isOnboardingPlugin || isSupportPlugin } + var isLoopPlugin: Bool { isPumpManagerPlugin || isCGMManagerPlugin || isStatefulPlugin || isServicePlugin || isOnboardingPlugin || isSupportPlugin } var isLoopExtension: Bool { object(forInfoDictionaryKey: LoopPluginBundleKey.extensionIdentifier.rawValue) as? String != nil } diff --git a/Loop/View Controllers/InsulinDeliveryTableViewController.swift b/Loop/View Controllers/InsulinDeliveryTableViewController.swift index 7a3f9115b3..c340f8f536 100644 --- a/Loop/View Controllers/InsulinDeliveryTableViewController.swift +++ b/Loop/View Controllers/InsulinDeliveryTableViewController.swift @@ -609,7 +609,7 @@ fileprivate var numberFormatter: NumberFormatter { fileprivate func createAttributedDescription(from description: String, with font: UIFont) -> NSAttributedString? { let descriptionWithFont = String(format:"%@", description) - guard let attributedDescription = try? NSMutableAttributedString(data: Data(descriptionWithFont.utf8), options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil) else { + guard let attributedDescription = try? NSMutableAttributedString(data: descriptionWithFont.data(using: .utf16)!, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil) else { return nil } diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index 74b49790aa..6a4aadfcdd 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -1385,22 +1385,46 @@ final class StatusTableViewController: LoopChartsTableViewController { @IBAction func presentBolusScreen() { presentBolusEntryView() } - - func presentBolusEntryView(enableManualGlucoseEntry: Bool = false) { - let hostingController: DismissibleHostingController + + @ViewBuilder + func bolusEntryView(enableManualGlucoseEntry: Bool = false) -> some View { if FeatureFlags.simpleBolusCalculatorEnabled && !automaticDosingStatus.automaticDosingEnabled { - let viewModel = SimpleBolusViewModel(delegate: deviceManager, displayMealEntry: false) - let bolusEntryView = SimpleBolusView(viewModel: viewModel).environmentObject(deviceManager.displayGlucosePreference) - hostingController = DismissibleHostingController(rootView: bolusEntryView, isModalInPresentation: false) + SimpleBolusView( + viewModel: SimpleBolusViewModel( + delegate: deviceManager, + displayMealEntry: false + ) + ) + .environmentObject(deviceManager.displayGlucosePreference) } else { - let viewModel = BolusEntryViewModel(delegate: deviceManager, screenWidth: UIScreen.main.bounds.width, isManualGlucoseEntryEnabled: enableManualGlucoseEntry) - Task { @MainActor in - await viewModel.generateRecommendationAndStartObserving() - } - viewModel.analyticsServicesManager = deviceManager.analyticsServicesManager - let bolusEntryView = BolusEntryView(viewModel: viewModel).environmentObject(deviceManager.displayGlucosePreference) - hostingController = DismissibleHostingController(rootView: bolusEntryView, isModalInPresentation: false) + let viewModel: BolusEntryViewModel = { + let viewModel = BolusEntryViewModel( + delegate: deviceManager, + screenWidth: UIScreen.main.bounds.width, + isManualGlucoseEntryEnabled: enableManualGlucoseEntry + ) + + Task { @MainActor in + await viewModel.generateRecommendationAndStartObserving() + } + + viewModel.analyticsServicesManager = deviceManager.analyticsServicesManager + + return viewModel + }() + + BolusEntryView(viewModel: viewModel) + .environmentObject(deviceManager.displayGlucosePreference) } + } + + func presentBolusEntryView(enableManualGlucoseEntry: Bool = false) { + let hostingController = DismissibleHostingController( + content: bolusEntryView( + enableManualGlucoseEntry: enableManualGlucoseEntry + ) + ) + let navigationWrapper = UINavigationController(rootViewController: hostingController) hostingController.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: navigationWrapper, action: #selector(dismissWithAnimation)) present(navigationWrapper, animated: true) @@ -1841,15 +1865,6 @@ final class StatusTableViewController: LoopChartsTableViewController { lastOrientation = UIDevice.current.orientation } - override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) { - guard FeatureFlags.allowDebugFeatures else { - return - } - if motion == .motionShake { - presentDebugMenu() - } - } - private func presentDebugMenu() { guard FeatureFlags.allowDebugFeatures else { return @@ -2231,7 +2246,7 @@ extension StatusTableViewController: ServicesViewModelDelegate { } func gotoService(withIdentifier identifier: String) { - guard let serviceUI = deviceManager.servicesManager.activeServices.first(where: { $0.serviceIdentifier == identifier }) as? ServiceUI else { + guard let serviceUI = deviceManager.servicesManager.activeServices.first(where: { $0.pluginIdentifier == identifier }) as? ServiceUI else { return } showServiceSettings(serviceUI) diff --git a/Loop/View Models/CarbEntryViewModel.swift b/Loop/View Models/CarbEntryViewModel.swift index d04ddba78e..37dedee326 100644 --- a/Loop/View Models/CarbEntryViewModel.swift +++ b/Loop/View Models/CarbEntryViewModel.swift @@ -59,10 +59,10 @@ final class CarbEntryViewModel: ObservableObject { @Published var time = Date() private var date = Date() var minimumDate: Date { - get { date.addingTimeInterval(.hours(-12)) } + get { date.addingTimeInterval(LoopConstants.maxCarbEntryPastTime) } } var maximumDate: Date { - get { date.addingTimeInterval(.hours(1)) } + get { date.addingTimeInterval(LoopConstants.maxCarbEntryFutureTime) } } @Published var foodType = "" @@ -140,7 +140,7 @@ final class CarbEntryViewModel: ObservableObject { var saveFavoriteFoodButtonDisabled: Bool { get { - if let carbsQuantity, 0...maxCarbEntryQuantity.doubleValue(for: preferredCarbUnit) ~= carbsQuantity, foodType != "", selectedFavoriteFoodIndex == -1 { + if let carbsQuantity, 0...maxCarbEntryQuantity.doubleValue(for: preferredCarbUnit) ~= carbsQuantity, selectedFavoriteFoodIndex == -1 { return false } return true @@ -290,13 +290,14 @@ final class CarbEntryViewModel: ObservableObject { } private func checkIfOverrideEnabled() { - if let managerSettings = delegate?.settings { - if let overrideSettings = managerSettings.scheduleOverride?.settings, overrideSettings.effectiveInsulinNeedsScaleFactor != 1.0 { - self.warnings.insert(.overrideInProgress) - } - else { - self.warnings.remove(.overrideInProgress) - } + if let managerSettings = delegate?.settings, + managerSettings.scheduleOverrideEnabled(at: Date()), + let overrideSettings = managerSettings.scheduleOverride?.settings, + overrideSettings.effectiveInsulinNeedsScaleFactor != 1.0 { + self.warnings.insert(.overrideInProgress) + } + else { + self.warnings.remove(.overrideInProgress) } } diff --git a/Loop/View Models/ServicesViewModel.swift b/Loop/View Models/ServicesViewModel.swift index d59e1e6603..19fb2a7d57 100644 --- a/Loop/View Models/ServicesViewModel.swift +++ b/Loop/View Models/ServicesViewModel.swift @@ -24,7 +24,7 @@ public class ServicesViewModel: ObservableObject { var inactiveServices: () -> [ServiceDescriptor] { return { return self.availableServices().filter { availableService in - !self.activeServices().contains { $0.serviceIdentifier == availableService.identifier } + !self.activeServices().contains { $0.pluginIdentifier == availableService.identifier } } } } @@ -42,7 +42,7 @@ public class ServicesViewModel: ObservableObject { } func didTapService(_ index: Int) { - delegate?.gotoService(withIdentifier: activeServices()[index].serviceIdentifier) + delegate?.gotoService(withIdentifier: activeServices()[index].pluginIdentifier) } func didTapAddService(_ availableService: ServiceDescriptor) { @@ -54,23 +54,25 @@ public class ServicesViewModel: ObservableObject { extension ServicesViewModel { fileprivate class FakeService1: Service { static var localizedTitle: String = "Service 1" - static var serviceIdentifier: String = "FakeService1" + static var pluginIdentifier: String = "FakeService1" + var stateDelegate: StatefulPluggableDelegate? var serviceDelegate: ServiceDelegate? var rawState: RawStateValue = [:] required init() {} required init?(rawState: RawStateValue) {} let isOnboarded = true - var available: ServiceDescriptor { ServiceDescriptor(identifier: serviceIdentifier, localizedTitle: localizedTitle) } + var available: ServiceDescriptor { ServiceDescriptor(identifier: pluginIdentifier, localizedTitle: localizedTitle) } } fileprivate class FakeService2: Service { static var localizedTitle: String = "Service 2" - static var serviceIdentifier: String = "FakeService2" + static var pluginIdentifier: String = "FakeService2" + var stateDelegate: StatefulPluggableDelegate? var serviceDelegate: ServiceDelegate? var rawState: RawStateValue = [:] required init() {} required init?(rawState: RawStateValue) {} let isOnboarded = true - var available: ServiceDescriptor { ServiceDescriptor(identifier: serviceIdentifier, localizedTitle: localizedTitle) } + var available: ServiceDescriptor { ServiceDescriptor(identifier: pluginIdentifier, localizedTitle: localizedTitle) } } static var preview: ServicesViewModel { diff --git a/Loop/Views/AddEditFavoriteFoodView.swift b/Loop/Views/AddEditFavoriteFoodView.swift index b647523a13..ddd01320b9 100644 --- a/Loop/Views/AddEditFavoriteFoodView.swift +++ b/Loop/Views/AddEditFavoriteFoodView.swift @@ -93,20 +93,40 @@ struct AddEditFavoriteFoodView: View { let foodTypeFocused: Binding = Binding(get: { expandedRow == .foodType }, set: { expandedRow = $0 ? .foodType : nil }) let absorptionTimeFocused: Binding = Binding(get: { expandedRow == .absorptionTime }, set: { expandedRow = $0 ? .absorptionTime : nil }) - TextFieldRow(text: $viewModel.name, isFocused: nameFocused, title: "Name", placeholder: "Apple") + TextFieldRow( + text: $viewModel.name, + isFocused: nameFocused, + title: NSLocalizedString("Name", comment: "Label for name in favorite food entry screen"), + placeholder: NSLocalizedString("Apple", comment: "Placeholder for name in favorite food entry screen") + ) CardSectionDivider() - CarbQuantityRow(quantity: $viewModel.carbsQuantity, isFocused: carbQuantityFocused, title: "Carb Quantity", preferredCarbUnit: viewModel.preferredCarbUnit) + CarbQuantityRow( + quantity: $viewModel.carbsQuantity, + isFocused: carbQuantityFocused, + title: NSLocalizedString("Carb Quantity", comment: "Label for carb quantity in favorite food entry screen"), + preferredCarbUnit: viewModel.preferredCarbUnit + ) CardSectionDivider() - EmojiRow(text: $viewModel.foodType, isFocused: foodTypeFocused, emojiType: .food, title: "Food Type") + EmojiRow( + text: $viewModel.foodType, + isFocused: foodTypeFocused, + emojiType: .food, + title: NSLocalizedString("Food Type", comment: "Label for food type in favorite entry screen") + ) CardSectionDivider() - AbsorptionTimePickerRow(absorptionTime: $viewModel.absorptionTime, isFocused: absorptionTimeFocused, validDurationRange: viewModel.absorptionRimesRange, showHowAbsorptionTimeWorks: $showHowAbsorptionTimeWorks) - .padding(.bottom, 2) + AbsorptionTimePickerRow( + absorptionTime: $viewModel.absorptionTime, + isFocused: absorptionTimeFocused, + validDurationRange: viewModel.absorptionRimesRange, + showHowAbsorptionTimeWorks: $showHowAbsorptionTimeWorks + ) + .padding(.bottom, 2) } .padding(.vertical, 12) .padding(.horizontal) diff --git a/Loop/Views/AlertManagementView.swift b/Loop/Views/AlertManagementView.swift index 856db6da11..e9a38e72a0 100644 --- a/Loop/Views/AlertManagementView.swift +++ b/Loop/Views/AlertManagementView.swift @@ -72,6 +72,75 @@ struct AlertManagementView: View { } .navigationTitle(NSLocalizedString("Alert Management", comment: "Title of alert management screen")) } + + private var footerView: some View { + VStack(alignment: .leading, spacing: 24) { + HStack(alignment: .top, spacing: 8) { + Image("phone") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: 64, maxHeight: 64) + + VStack(alignment: .leading, spacing: 4) { + Text( + String( + format: NSLocalizedString( + "%1$@ APP SOUNDS", + comment: "App sounds title text (1: app name)" + ), + appName.uppercased() + ) + ) + + Text( + String( + format: NSLocalizedString( + "While mute alerts is on, all alerts from your %1$@ app including Critical and Time Sensitive alerts will temporarily display without sounds and will vibrate only.", + comment: "App sounds descriptive text (1: app name)" + ), + appName + ) + ) + } + } + + HStack(alignment: .top, spacing: 8) { + Image("hardware") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: 64, maxHeight: 64) + + VStack(alignment: .leading, spacing: 4) { + Text("HARDWARE SOUNDS") + + Text("While mute alerts is on, your insulin pump and CGM hardware may still sound.") + } + } + + HStack(alignment: .top, spacing: 8) { + Image(systemName: "moon.fill") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: 64, maxHeight: 48) + .foregroundColor(.accentColor) + + VStack(alignment: .leading, spacing: 4) { + Text("IOS FOCUS MODES") + + Text( + String( + format: NSLocalizedString( + "If iOS Focus Mode is ON and Mute Alerts is OFF, Critical Alerts will still be delivered and non-Critical Alerts will be silenced until %1$@ is added to each Focus mode as an Allowed App.", + comment: "Focus modes descriptive text (1: app name)" + ), + appName + ) + ) + } + } + } + .padding(.top) + } private var alertPermissionsSection: some View { Section(footer: DescriptiveText(label: String(format: NSLocalizedString("Notifications give you important %1$@ app information without requiring you to open the app.", comment: "Alert Permissions descriptive text (1: app name)"), appName))) { @@ -93,7 +162,7 @@ struct AlertManagementView: View { @ViewBuilder private var muteAlertsSection: some View { - Section(footer: DescriptiveText(label: String(format: NSLocalizedString("When muted, %1$@ alerts will temporarily display without sounds and will vibrate only. Once the mute period ends, your alerts will resume as normal.", comment: "Description of temporary mute alerts (1: app name)"), appName))) { + Section(footer: footerView) { if !alertMuter.configuration.shouldMute { howMuteAlertsWork Button(action: { showMuteAlertOptions = true }) { @@ -142,7 +211,7 @@ struct AlertManagementView: View { private var howMuteAlertsWork: some View { Button(action: { showHowMuteAlertWork = true }) { HStack { - Text(NSLocalizedString("Take a closer look at how mute alerts works", comment: "Label for link to learn how mute alerts work")) + Text(NSLocalizedString("Frequently asked questions about alerts", comment: "Label for link to see frequently asked questions")) .font(.footnote) .foregroundColor(.secondary) Spacer() diff --git a/Loop/Views/FavoriteFoodDetailView.swift b/Loop/Views/FavoriteFoodDetailView.swift index 44c7a83150..a0fe7d3eef 100644 --- a/Loop/Views/FavoriteFoodDetailView.swift +++ b/Loop/Views/FavoriteFoodDetailView.swift @@ -35,10 +35,10 @@ public struct FavoriteFoodDetailView: View { Section("Information") { VStack(spacing: 16) { let rows: [(field: String, value: String)] = [ - ("Name", food.name), - ("Carb Quantity", food.carbsString(formatter: carbFormatter)), - ("Food Type", food.foodType), - ("Absorption Time", food.absorptionTimeString(formatter: absorptionTimeFormatter)) + (NSLocalizedString("Name", comment: "Label for name in favorite food entry"), food.name), + (NSLocalizedString("Carb Quantity", comment:"Label for carb quantity in favorite food entry"), food.carbsString(formatter: carbFormatter)), + (NSLocalizedString("Food Type", comment:"Label for food type in favorite food entry"), food.foodType), + (NSLocalizedString("Absorption Time", comment:"Label for absorption time in favorite food entry"), food.absorptionTimeString(formatter: absorptionTimeFormatter)) ] ForEach(rows, id: \.field) { row in HStack { diff --git a/Loop/Views/HowMuteAlertWorkView.swift b/Loop/Views/HowMuteAlertWorkView.swift index 4f5932d36b..08443a6b80 100644 --- a/Loop/Views/HowMuteAlertWorkView.swift +++ b/Loop/Views/HowMuteAlertWorkView.swift @@ -17,52 +17,109 @@ struct HowMuteAlertWorkView: View { var body: some View { NavigationView { List { - VStack(alignment: .leading) { - Text(NSLocalizedString(""" -Mute Alerts allows you to temporarily silence your alerts and alarms. - -When using Mute Alerts, also consider the impact of using iOS Focus Modes. -""", comment: "Description of how mute alerts work")) - .fixedSize(horizontal: false, vertical: true) + VStack(alignment: .leading, spacing: 24) { + VStack(alignment: .leading, spacing: 8) { + Text("What are examples of Critical and Time Sensitive alerts?") + .bold() + + Text("iOS Critical Alerts and Time Sensitive Alerts are types of Apple notifications. They are used for high-priority events. Some examples include:") + } - VStack(alignment: .leading, spacing: 10) { - HStack(spacing: 10) { - Image(systemName: "speaker.slash.fill") - .foregroundColor(.white) - .padding(5) - .background(guidanceColors.warning) - .clipShape(RoundedRectangle(cornerRadius: 5, style: .continuous)) + HStack { + VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading, spacing: 4) { + Text("Critical Alerts") + .bold() + + Text("Urgent Low") + .bulleted() + Text("Sensor Failed") + .bulleted() + Text("Reservoir Empty") + .bulleted() + Text("Pump Expired") + .bulleted() + } - Text(String(format: NSLocalizedString("%1$@ Mute Alerts", comment: "Format string for Section title for description that mute alerts is temporary (1: app name)"), appName)) - .bold() - .fixedSize(horizontal: false, vertical: true) + VStack(alignment: .leading, spacing: 4) { + Text("Time Sensitive Alerts") + .bold() + + Text("High Glucose") + .bulleted() + Text("Transmitter Low Battery") + .bulleted() + } } - Text(NSLocalizedString(""" -All Tidepool Loop alerts, including Critical Alerts, will be silenced for up to 4 hours. - -After the mute period ends, your alert sounds will resume. -""", comment: "Description that mute alerts is temporary")) - .fixedSize(horizontal: false, vertical: true) - .padding(.bottom) - - HStack(spacing: 10) { - Image(systemName: "moon.fill") - .foregroundColor(.accentColor) - - Text(NSLocalizedString("iOS Focus Mode", comment: "Section title for description of how mute alerts work with focus mode")) - .bold() - } - Text(String(format: NSLocalizedString("If iOS Focus Mode is ON and Mute Alerts is OFF, Critical Alerts will still be delivered, but non-Critical Alerts will be silenced until %1$@ is added to each Focus mode as an Allowed App.", comment: "Format string for description of how mute alerts works with focus mode (1: app name)"), appName)) - .fixedSize(horizontal: false, vertical: true) + Spacer() } + .font(.footnote) + .foregroundColor(.black.opacity(0.6)) .padding() - .overlay(RoundedRectangle(cornerRadius: 10, style: .continuous).stroke(Color(.systemFill), lineWidth: 1)) + .overlay( + RoundedRectangle(cornerRadius: 10, style: .continuous) + .stroke(Color(.systemFill), lineWidth: 1) + ) .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) + + VStack(alignment: .leading, spacing: 8) { + Text( + String( + format: NSLocalizedString( + "How can I temporarily silence all %1$@ app sounds?", + comment: "Title text for temporarily silencing all sounds (1: app name)" + ), + appName + ) + ) + .bold() + + Text( + String( + format: NSLocalizedString( + "Use the Mute Alerts feature. It allows you to temporarily silence all of your alerts and alarms via the %1$@ app, including Critical Alerts and Time Sensitive Alerts.", + comment: "Description text for temporarily silencing all sounds (1: app name)" + ), + appName + ) + ) + } + + VStack(alignment: .leading, spacing: 8) { + Text("How can I silence non-Critical Alerts?") + .bold() + + Text( + String( + format: NSLocalizedString( + "Turn off the volume on your iOS device or add %1$@ as an allowed app to each Focus Mode. Time Sensitive and Critical Alerts will still sound, but non-Critical Alerts will be silenced.", + comment: "Description text for temporarily silencing non-critical alerts (1: app name)" + ), + appName + ) + ) + } + + VStack(alignment: .leading, spacing: 8) { + Text("How can I silence only Time Sensitive and Non-Critical alerts?") + .bold() + + Text( + String( + format: NSLocalizedString( + "For safety purposes, you should allow Critical Alerts, Time Sensitive and Notification Permissions (non-critical alerts) on your device to continue using %1$@ and cannot turn off individual alarms.", + comment: "Description text for silencing time sensitive and non-critical alerts (1: app name)" + ), + appName + ) + ) + } } + .padding(.vertical, 8) } .insetGroupedListStyle() - .navigationTitle(NSLocalizedString("Using Mute Alerts", comment: "View title for how mute alerts work")) + .navigationTitle(NSLocalizedString("Managing Alerts", comment: "View title for how mute alerts work")) .navigationBarItems(trailing: closeButton) } } @@ -74,6 +131,19 @@ After the mute period ends, your alert sounds will resume. } } +private extension Text { + func bulleted(color: Color = .accentColor.opacity(0.5)) -> some View { + HStack(spacing: 16) { + Image(systemName: "circle.fill") + .resizable() + .frame(width: 8, height: 8) + .foregroundColor(color) + + self + } + } +} + struct HowMuteAlertWorkView_Previews: PreviewProvider { static var previews: some View { HowMuteAlertWorkView() diff --git a/Loop/Views/NotificationsCriticalAlertPermissionsView.swift b/Loop/Views/NotificationsCriticalAlertPermissionsView.swift index e69084a46a..b9e1552036 100644 --- a/Loop/Views/NotificationsCriticalAlertPermissionsView.swift +++ b/Loop/Views/NotificationsCriticalAlertPermissionsView.swift @@ -32,8 +32,8 @@ public struct NotificationsCriticalAlertPermissionsView: View { public var body: some View { switch mode { - case .flow: return AnyView(content()) - case .topLevel: return AnyView(navigationContent()) + case .flow: content() + case .topLevel: navigationContent() } } diff --git a/Loop/Views/SettingsView.swift b/Loop/Views/SettingsView.swift index 0b4cb55133..ed9723d243 100644 --- a/Loop/Views/SettingsView.swift +++ b/Loop/Views/SettingsView.swift @@ -24,13 +24,39 @@ public struct SettingsView: View { @ObservedObject var viewModel: SettingsViewModel @ObservedObject var versionUpdateViewModel: VersionUpdateViewModel - @State private var pumpChooserIsPresented: Bool = false - @State private var cgmChooserIsPresented: Bool = false - @State private var favoriteFoodsIsPresented: Bool = false - @State private var serviceChooserIsPresented: Bool = false - @State private var therapySettingsIsPresented: Bool = false - @State private var deletePumpDataAlertIsPresented = false - @State private var deleteCGMDataAlertIsPresented = false + enum Destination { + enum Alert: String, Identifiable { + var id: String { + rawValue + } + + case deleteCGMData + case deletePumpData + } + + enum ActionSheet: String, Identifiable { + var id: String { + rawValue + } + + case cgmPicker + case pumpPicker + case servicePicker + } + + enum Sheet: String, Identifiable { + var id: String { + rawValue + } + + case favoriteFoods + case therapySettings + } + } + + @State private var actionSheet: Destination.ActionSheet? + @State private var alert: Destination.Alert? + @State private var sheet: Destination.Sheet? var localizedAppNameAndVersion: String @@ -75,13 +101,64 @@ public struct SettingsView: View { supportSection if let profileExpiration = BuildDetails.default.profileExpiration, FeatureFlags.profileExpirationSettingsViewEnabled { - profileExpirationSection(profileExpiration: profileExpiration) + appExpirationSection(profileExpiration: profileExpiration) } } } .insetGroupedListStyle() .navigationBarTitle(Text(NSLocalizedString("Settings", comment: "Settings screen title"))) .navigationBarItems(trailing: dismissButton) + .actionSheet(item: $actionSheet) { actionSheet in + switch actionSheet { + case .cgmPicker: + return ActionSheet( + title: Text("Add CGM", comment: "The title of the CGM chooser in settings"), + buttons: cgmChoices + ) + case .pumpPicker: + return ActionSheet( + title: Text("Add Pump", comment: "The title of the pump chooser in settings"), + buttons: pumpChoices + ) + case .servicePicker: + return ActionSheet( + title: Text("Add Service", comment: "The title of the add service action sheet in settings"), + buttons: serviceChoices + ) + } + } + .alert(item: $alert) { alert in + switch alert { + case .deleteCGMData: + return makeDeleteAlert(for: self.viewModel.cgmManagerSettingsViewModel) + case .deletePumpData: + return makeDeleteAlert(for: self.viewModel.pumpManagerSettingsViewModel) + } + } + .sheet(item: $sheet) { sheet in + switch sheet { + case .therapySettings: + TherapySettingsView( + mode: .settings, + viewModel: TherapySettingsViewModel( + therapySettings: viewModel.therapySettings(), + sensitivityOverridesEnabled: FeatureFlags.sensitivityOverridesEnabled, + adultChildInsulinModelSelectionEnabled: FeatureFlags.adultChildInsulinModelSelectionEnabled, + delegate: viewModel.therapySettingsViewModelDelegate + ) + ) + .environmentObject(displayGlucosePreference) + .environment(\.dismissAction, self.dismiss) + .environment(\.appName, self.appName) + .environment(\.chartColorPalette, .primary) + .environment(\.carbTintColor, self.carbTintColor) + .environment(\.glucoseTintColor, self.glucoseTintColor) + .environment(\.guidanceColors, self.guidanceColors) + .environment(\.insulinTintColor, self.insulinTintColor) + case .favoriteFoods: + FavoriteFoodsView() + } + } } .navigationViewStyle(.stack) } @@ -119,13 +196,13 @@ extension String: Identifiable { } } -struct PluginMenuItem: Identifiable { +struct PluginMenuItem: Identifiable { var id: String { return pluginIdentifier + String(describing: offset) } let section: SettingsMenuSection - let view: AnyView + let view: Content let pluginIdentifier: String let offset: Int } @@ -178,52 +255,45 @@ extension SettingsView { } } + @ViewBuilder + private var alertWarning: some View { + if viewModel.alertPermissionsChecker.showWarning || viewModel.alertPermissionsChecker.notificationCenterSettings.scheduledDeliveryEnabled { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundColor(.critical) + } else if viewModel.alertMuter.configuration.shouldMute { + Image(systemName: "speaker.slash.fill") + .foregroundColor(.white) + .padding(5) + .background(guidanceColors.warning) + .clipShape(RoundedRectangle(cornerRadius: 5, style: .continuous)) + } + } + private var alertManagementSection: some View { Section { - NavigationLink(destination: AlertManagementView(checker: viewModel.alertPermissionsChecker, alertMuter: viewModel.alertMuter)) - { - HStack { - Text(NSLocalizedString("Alert Management", comment: "Alert Permissions button text")) - if viewModel.alertPermissionsChecker.showWarning || - viewModel.alertPermissionsChecker.notificationCenterSettings.scheduledDeliveryEnabled { - Spacer() - Image(systemName: "exclamationmark.triangle.fill") - .foregroundColor(.critical) - } else if viewModel.alertMuter.configuration.shouldMute { - Spacer() - Image(systemName: "speaker.slash.fill") - .foregroundColor(.white) - .padding(5) - .background(guidanceColors.warning) - .clipShape(RoundedRectangle(cornerRadius: 5, style: .continuous)) - } - } + NavigationLink(destination: AlertManagementView(checker: viewModel.alertPermissionsChecker, alertMuter: viewModel.alertMuter)) { + LargeButton( + action: {}, + includeArrow: false, + imageView: Image(systemName: "bell.fill") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 30), + secondaryImageView: alertWarning, + label: NSLocalizedString("Alert Management", comment: "Alert Permissions button text"), + descriptiveText: NSLocalizedString("Alert Permissions and Mute Alerts", comment: "Alert Permissions descriptive text") + ) } } } private var configurationSection: some View { Section(header: SectionHeader(label: NSLocalizedString("Configuration", comment: "The title of the Configuration section in settings"))) { - LargeButton(action: { self.therapySettingsIsPresented = true }, + LargeButton(action: { sheet = .therapySettings }, includeArrow: true, - imageView: AnyView(Image("Therapy Icon")), + imageView: Image("Therapy Icon"), label: NSLocalizedString("Therapy Settings", comment: "Title text for button to Therapy Settings"), descriptiveText: NSLocalizedString("Diabetes Treatment", comment: "Descriptive text for Therapy Settings")) - .sheet(isPresented: $therapySettingsIsPresented) { - TherapySettingsView(mode: .settings, - viewModel: TherapySettingsViewModel(therapySettings: self.viewModel.therapySettings(), - sensitivityOverridesEnabled: FeatureFlags.sensitivityOverridesEnabled, - adultChildInsulinModelSelectionEnabled: FeatureFlags.adultChildInsulinModelSelectionEnabled, - delegate: self.viewModel.therapySettingsViewModelDelegate)) - .environmentObject(displayGlucosePreference) - .environment(\.dismissAction, self.dismiss) - .environment(\.appName, self.appName) - .environment(\.chartColorPalette, .primary) - .environment(\.carbTintColor, self.carbTintColor) - .environment(\.glucoseTintColor, self.glucoseTintColor) - .environment(\.guidanceColors, self.guidanceColors) - .environment(\.insulinTintColor, self.insulinTintColor) - } ForEach(pluginMenuItems.filter {$0.section == .configuration}) { item in item.view @@ -235,10 +305,10 @@ extension SettingsView { } } - private var pluginMenuItems: [PluginMenuItem] { + private var pluginMenuItems: [PluginMenuItem] { self.viewModel.availableSupports.flatMap { plugin in plugin.configurationMenuItems().enumerated().map { index, item in - PluginMenuItem(section: item.section, view: item.view, pluginIdentifier: plugin.identifier, offset: index) + PluginMenuItem(section: item.section, view: item.view, pluginIdentifier: plugin.pluginIdentifier, offset: index) } } } @@ -259,16 +329,11 @@ extension SettingsView { label: viewModel.pumpManagerSettingsViewModel.name(), descriptiveText: NSLocalizedString("Insulin Pump", comment: "Descriptive text for Insulin Pump")) } else if viewModel.isOnboardingComplete { - LargeButton(action: { self.pumpChooserIsPresented = true }, + LargeButton(action: { actionSheet = .pumpPicker }, includeArrow: false, - imageView: AnyView(plusImage), + imageView: plusImage, label: NSLocalizedString("Add Pump", comment: "Title text for button to add pump device"), descriptiveText: NSLocalizedString("Tap here to set up a pump", comment: "Descriptive text for button to add pump device")) - .actionSheet(isPresented: $pumpChooserIsPresented) { - ActionSheet(title: Text("Add Pump", comment: "The title of the pump chooser in settings"), buttons: pumpChoices) - } - } else { - EmptyView() } } @@ -291,27 +356,21 @@ extension SettingsView { label: viewModel.cgmManagerSettingsViewModel.name(), descriptiveText: NSLocalizedString("Continuous Glucose Monitor", comment: "Descriptive text for Continuous Glucose Monitor")) } else { - LargeButton(action: { self.cgmChooserIsPresented = true }, + LargeButton(action: { actionSheet = .cgmPicker }, includeArrow: false, - imageView: AnyView(plusImage), + imageView: plusImage, label: NSLocalizedString("Add CGM", comment: "Title text for button to add CGM device"), descriptiveText: NSLocalizedString("Tap here to set up a CGM", comment: "Descriptive text for button to add CGM device")) - .actionSheet(isPresented: $cgmChooserIsPresented) { - ActionSheet(title: Text("Add CGM", comment: "The title of the CGM chooser in settings"), buttons: cgmChoices) - } } } private var favoriteFoodsSection: some View { Section { - LargeButton(action: { self.favoriteFoodsIsPresented = true }, + LargeButton(action: { sheet = .favoriteFoods }, includeArrow: true, - imageView: AnyView(Image("Favorite Foods Icon").renderingMode(.template).foregroundColor(carbTintColor)), - label: "Favorite Foods", - descriptiveText: "Simplify Carb Entry") - } - .sheet(isPresented: $favoriteFoodsIsPresented) { - FavoriteFoodsView() + imageView: Image("Favorite Foods Icon").renderingMode(.template).foregroundColor(carbTintColor), + label: NSLocalizedString("Favorite Foods", comment: "Label for favorite foods in settings view"), + descriptiveText: NSLocalizedString("Simplify Carb Entry", comment: "subheadline of favorite foods in settings view")) } } @@ -337,14 +396,11 @@ extension SettingsView { descriptiveText: "") } if viewModel.servicesViewModel.inactiveServices().count > 0 { - LargeButton(action: { self.serviceChooserIsPresented = true }, + LargeButton(action: { actionSheet = .servicePicker }, includeArrow: false, - imageView: AnyView(plusImage), + imageView: plusImage, label: NSLocalizedString("Add Service", comment: "The title of the add service button in settings"), descriptiveText: NSLocalizedString("Tap here to set up a Service", comment: "The descriptive text of the add service button in settings")) - .actionSheet(isPresented: $serviceChooserIsPresented) { - ActionSheet(title: Text("Add Service", comment: "The title of the add service action sheet in settings"), buttons: serviceChoices) - } } } } @@ -362,28 +418,22 @@ extension SettingsView { private var deleteDataSection: some View { Section { if viewModel.pumpManagerSettingsViewModel.isTestingDevice { - Button(action: { self.deletePumpDataAlertIsPresented.toggle() }) { + Button(action: { alert = .deletePumpData }) { HStack { Spacer() Text("Delete Testing Pump Data").accentColor(.destructive) Spacer() } } - .alert(isPresented: $deletePumpDataAlertIsPresented) { - makeDeleteAlert(for: self.viewModel.pumpManagerSettingsViewModel) - } } if viewModel.cgmManagerSettingsViewModel.isTestingDevice { - Button(action: { self.deleteCGMDataAlertIsPresented.toggle() }) { + Button(action: { alert = .deleteCGMData }) { HStack { Spacer() Text("Delete Testing CGM Data").accentColor(.destructive) Spacer() } } - .alert(isPresented: $deleteCGMDataAlertIsPresented) { - makeDeleteAlert(for: self.viewModel.cgmManagerSettingsViewModel) - } } } } @@ -416,24 +466,50 @@ extension SettingsView { /* DIY loop specific component to show users the amount of time remaining on their build before a rebuild is necessary. */ - private func profileExpirationSection(profileExpiration:Date) -> some View { - let nearExpiration : Bool = ProfileExpirationAlerter.isNearProfileExpiration(profileExpiration: profileExpiration) - let profileExpirationMsg = ProfileExpirationAlerter.createProfileExpirationSettingsMessage(profileExpiration: profileExpiration) - let readableExpirationTime = Self.dateFormatter.string(from: profileExpiration) + private func appExpirationSection(profileExpiration: Date) -> some View { + let expirationDate = AppExpirationAlerter.calculateExpirationDate(profileExpiration: profileExpiration) + let isTestFlight = AppExpirationAlerter.isTestFlightBuild() + let nearExpiration = AppExpirationAlerter.isNearExpiration(expirationDate: expirationDate) + let profileExpirationMsg = AppExpirationAlerter.createProfileExpirationSettingsMessage(expirationDate: expirationDate) + let readableExpirationTime = Self.dateFormatter.string(from: expirationDate) - return Section(header: SectionHeader(label: NSLocalizedString("App Profile", comment: "Settings app profile section")), - footer: Text(NSLocalizedString("Profile expires ", comment: "Time that profile expires") + readableExpirationTime)) { - if(nearExpiration) { - Text(profileExpirationMsg).foregroundColor(.red) + if isTestFlight { + return createAppExpirationSection( + headerLabel: NSLocalizedString("TestFlight", comment: "Settings app TestFlight section"), + footerLabel: NSLocalizedString("TestFlight expires ", comment: "Time that build expires") + readableExpirationTime, + expirationLabel: NSLocalizedString("TestFlight Expiration", comment: "Settings TestFlight expiration view"), + updateURL: "https://loopkit.github.io/loopdocs/gh-actions/gh-update/", + nearExpiration: nearExpiration, + expirationMessage: profileExpirationMsg + ) + } else { + return createAppExpirationSection( + headerLabel: NSLocalizedString("App Profile", comment: "Settings app profile section"), + footerLabel: NSLocalizedString("Profile expires ", comment: "Time that profile expires") + readableExpirationTime, + expirationLabel: NSLocalizedString("Profile Expiration", comment: "Settings App Profile expiration view"), + updateURL: "https://loopkit.github.io/loopdocs/build/updating/", + nearExpiration: nearExpiration, + expirationMessage: profileExpirationMsg + ) + } + } + + private func createAppExpirationSection(headerLabel: String, footerLabel: String, expirationLabel: String, updateURL: String, nearExpiration: Bool, expirationMessage: String) -> some View { + return Section( + header: SectionHeader(label: headerLabel), + footer: Text(footerLabel) + ) { + if nearExpiration { + Text(expirationMessage).foregroundColor(.red) } else { HStack { - Text("Profile Expiration", comment: "Settings App Profile expiration view") + Text(expirationLabel) Spacer() - Text(profileExpirationMsg).foregroundColor(Color.secondary) + Text(expirationMessage).foregroundColor(Color.secondary) } } Button(action: { - UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/build/updating/")!) + UIApplication.shared.open(URL(string: updateURL)!) }) { Text(NSLocalizedString("How to update (LoopDocs)", comment: "The title text for how to update")) } @@ -455,54 +531,83 @@ extension SettingsView { .padding(EdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)) } - private func deviceImage(uiImage: UIImage?) -> AnyView { + @ViewBuilder + private func deviceImage(uiImage: UIImage?) -> some View { if let uiImage = uiImage { - return AnyView(Image(uiImage: uiImage) + Image(uiImage: uiImage) .renderingMode(.original) .resizable() - .scaledToFit()) + .scaledToFit() } else { - return AnyView(Spacer()) + Spacer() } } - private func serviceImage(uiImage: UIImage?) -> AnyView { - return deviceImage(uiImage: uiImage) + @ViewBuilder + private func serviceImage(uiImage: UIImage?) -> some View { + deviceImage(uiImage: uiImage) } } -fileprivate struct LargeButton: View { +fileprivate struct LargeButton: View { let action: () -> Void - var includeArrow: Bool = true - let imageView: AnyView + var includeArrow: Bool + let imageView: Content + let secondaryImageView: SecondaryContent let label: String let descriptiveText: String + init( + action: @escaping () -> Void, + includeArrow: Bool = true, + imageView: Content, + secondaryImageView: SecondaryContent = EmptyView(), + label: String, + descriptiveText: String + ) { + self.action = action + self.includeArrow = includeArrow + self.imageView = imageView + self.secondaryImageView = secondaryImageView + self.label = label + self.descriptiveText = descriptiveText + } + // TODO: The design doesn't show this, but do we need to consider different values here for different size classes? - static let spacing: CGFloat = 15 - static let imageWidth: CGFloat = 60 - static let imageHeight: CGFloat = 60 - static let topBottomPadding: CGFloat = 10 + private let spacing: CGFloat = 15 + private let imageWidth: CGFloat = 60 + private let imageHeight: CGFloat = 60 + private let secondaryImageWidth: CGFloat = 30 + private let secondaryImageHeight: CGFloat = 30 + private let topBottomPadding: CGFloat = 10 public var body: some View { Button(action: action) { HStack { - HStack(spacing: Self.spacing) { - imageView.frame(width: Self.imageWidth, height: Self.imageHeight) + HStack(spacing: spacing) { + imageView.frame(maxWidth: imageWidth, maxHeight: imageHeight) VStack(alignment: .leading) { Text(label) .foregroundColor(.primary) DescriptiveText(label: descriptiveText) } } - if includeArrow { + + if !(secondaryImageView is EmptyView) || includeArrow { Spacer() + } + + if !(secondaryImageView is EmptyView) { + secondaryImageView.frame(width: secondaryImageWidth, height: secondaryImageHeight) + } + + if includeArrow { // TODO: Ick. I can't use a NavigationLink because we're not Navigating, but this seems worse somehow. Image(systemName: "chevron.right").foregroundColor(.gray).font(.footnote) } } - .padding(EdgeInsets(top: Self.topBottomPadding, leading: 0, bottom: Self.topBottomPadding, trailing: 0)) + .padding(EdgeInsets(top: topBottomPadding, leading: 0, bottom: topBottomPadding, trailing: 0)) } } } diff --git a/Loop/Views/SimpleBolusView.swift b/Loop/Views/SimpleBolusView.swift index af9098abb6..2a7fc3fe59 100644 --- a/Loop/Views/SimpleBolusView.swift +++ b/Loop/Views/SimpleBolusView.swift @@ -369,12 +369,12 @@ struct SimpleBolusCalculatorView_Previews: PreviewProvider { func addCarbEntry(_ carbEntry: NewCarbEntry, replacing replacingEntry: StoredCarbEntry?, completion: @escaping (Result) -> Void) { let storedCarbEntry = StoredCarbEntry( + startDate: carbEntry.startDate, + quantity: carbEntry.quantity, uuid: UUID(), provenanceIdentifier: UUID().uuidString, syncIdentifier: UUID().uuidString, syncVersion: 1, - startDate: carbEntry.startDate, - quantity: carbEntry.quantity, foodType: carbEntry.foodType, absorptionTime: carbEntry.absorptionTime, createdByCurrentApp: true, diff --git a/Loop/ar.lproj/Localizable.strings b/Loop/ar.lproj/Localizable.strings index 5f46eb71e6..93db5473fa 100644 --- a/Loop/ar.lproj/Localizable.strings +++ b/Loop/ar.lproj/Localizable.strings @@ -306,6 +306,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "موافق"; diff --git a/Loop/cs.lproj/Localizable.strings b/Loop/cs.lproj/Localizable.strings index e4c3661250..effad5b9ea 100644 --- a/Loop/cs.lproj/Localizable.strings +++ b/Loop/cs.lproj/Localizable.strings @@ -67,6 +67,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "OK"; diff --git a/Loop/da.lproj/Localizable.strings b/Loop/da.lproj/Localizable.strings index 2bd3dbc3b9..45da938a4d 100644 --- a/Loop/da.lproj/Localizable.strings +++ b/Loop/da.lproj/Localizable.strings @@ -707,7 +707,8 @@ /* Details for missing data error when momentum effects are missing */ "Momentum effects" = "Momentumeffekter"; -/* Text for more info action on notification of upcoming profile expiration */ +/* Text for more info action on notification of upcoming profile expiration + Text for more info action on notification of upcoming TestFlight expiration */ "More Info" = "Mere Info"; /* Label for button to mute all alerts */ @@ -777,6 +778,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "OK"; diff --git a/Loop/de.lproj/Localizable.strings b/Loop/de.lproj/Localizable.strings index e775d574a1..b44b2ff1bc 100755 --- a/Loop/de.lproj/Localizable.strings +++ b/Loop/de.lproj/Localizable.strings @@ -125,6 +125,9 @@ /* Alert message for a missing pump error */ "A pump must be configured before a bolus can be delivered." = "Eine Pumpe muss konfiguriert werden, bevor ein Bolus abgegeben werden kann."; +/* Label for absorption time in favorite food entry */ +"Absorption Time" = "Resorptionsdauer"; + /* Action to copy the recommended Bolus value to the actual Bolus Field */ "AcceptRecommendedBolus" = "Akzeptiere empfohlenen Bolus"; @@ -143,6 +146,9 @@ /* The string format describing active insulin. (1: localized insulin value description) */ "Active Insulin: %@" = "Aktives Insulin: %@"; +/* No comment provided by engineer. */ +"Add a new favorite food" = "Erstelle einen neuen Favoriten"; + /* Title of the user activity for adding carbs */ "Add Carb Entry" = "KH hinzufügen"; @@ -171,9 +177,25 @@ Notification & Critical Alert Permissions screen title */ "Alert Permissions" = "Benachrichtigungsberechtigungen"; +/* Navigation title for algorithms experiments screen + The title of the Algorithm Experiments section in settings */ +"Algorithm Experiments" = "Algorithmusexperimente"; + +/* Algorithm Experiments description. */ +"Algorithm Experiments are optional modifications to the Loop Algorithm. These modifications are less tested than the standard Loop Algorithm, so please use carefully." = "Algorithmusexperimente sind optionale Modifikationen des Schleifenalgorithmus. Diese Modifikationen sind weniger getestet als der Standard-Loop-Algorithmus. Gehe daher bitte vorsichtig vor!"; + /* The title of the section containing algorithm settings */ "Algorithm Settings" = "Algorithmus-Einstellungen"; +/* No comment provided by engineer. */ +"All Favorites" = "Alle Favoriten"; + +/* Label for carb quantity entry row on carb entry screen */ +"Amount Consumed" = "Menge gegessen"; + +/* Label for when mute alert will end */ +"All alerts muted until" = "Alle Alarme stummgeschaltet bis"; + /* The title of the Amplitude service */ "Amplitude" = "Amplitude"; @@ -201,6 +223,9 @@ /* Settings app profile section */ "App Profile" = "App-Profil"; +/* Placeholder for name in favorite food entry screen */ +"Apple" = "Apfel"; + /* Action sheet confirmation message for pump history deletion */ "Are you sure you want to delete all history entries?" = "Möchtest Du wirklich alle Verlaufseinträge löschen?"; @@ -216,6 +241,9 @@ /* Confirmation message for deleting a CGM */ "Are you sure you want to delete this CGM?" = "Bist Du sicher, dass Du dieses CGM löschen möchtest?"; +/* No comment provided by engineer. */ +"Are you sure you want to delete this food?" = "Bist Du sicher, dass Du diesen Favoriten löschen möchtest?"; + /* Confirmation message for deleting a service */ "Are you sure you want to delete this service?" = "Bist Du sicher, dass Du diesen Dienst löschen möchtest?"; @@ -284,6 +312,10 @@ /* Label for carb entry row on bolus screen */ "Carb Entry" = "KH-Eintrag"; +/* Label for carb quantity entry row on favorite food entry screen + Label for carb quantity in favorite food entry */ +"Carb Quantity" = "KH-Menge"; + /* Details for configuration error when carb ratio schedule is missing */ "Carb Ratio Schedule" = "Zeitplan für das Kohlenhydratverhältnis"; @@ -303,6 +335,9 @@ /* Description of the prediction input effect for carbohydrates. (1: The glucose unit string) */ "Carbs Absorbed (g) ÷ Carb Ratio (g/U) × Insulin Sensitivity (%1$@/U)" = "Resorbierte Kohlenhydrate (g) ÷ Kohlenhydratfaktor (g/IE) × Insulinempfindlichkeit (%1$@/IE)"; +/* No comment provided by engineer. */ +"Caution" = "Achtung"; + /* The notification alert describing a low pump battery */ "Change the pump battery immediately" = "Wechsel sofort die Pumpenbatterie"; @@ -324,6 +359,9 @@ /* Carb entry section footer text explaining absorption time */ "Choose a longer absorption time for larger meals, or those containing fats and proteins. This is only guidance to the algorithm and need not be exact." = "Wähle eine längere Resorptionsdauer für größere Mahlzeiten oder welche die viel Fett und Proteine beinhalten. Dies ist eine Unterstützung für den Algorithmus und muss nicht genau sein."; +/* No comment provided by engineer. */ +"Choose Favorite:" = "Wähle Favorit:"; + /* Button title to close view The button label of the action used to dismiss the unsafe notification permission alert */ "Close" = "Schließen"; @@ -401,6 +439,9 @@ /* No comment provided by engineer. */ "Delete" = "Löschen"; +/* No comment provided by engineer. */ +"Delete “%@”?" = "„ %@ “ löschen?"; + /* The title of the button to remove the credentials for a service */ "Delete Account" = "Konto löschen"; @@ -410,6 +451,9 @@ /* Button title to delete CGM */ "Delete CGM" = "CGM löschen"; +/* No comment provided by engineer. */ +"Delete Food" = "Essen löschen"; + /* Button title to delete a service */ "Delete Service" = "Dienst löschen"; @@ -453,9 +497,18 @@ /* Override error description: duration exceed max (1: max duration in hours). */ "Duration exceeds: %1$.1f hours" = "Dauer überschritten: %1$.1f Stunden"; +/* No comment provided by engineer. */ +"Edit" = "Bearbeiten"; + /* Message to the user to enable bluetooth */ "Enable\nBluetooth" = "Bluetooth einschalten"; +/* Title for Glucose Based Partial Application toggle */ +"Enable Glucose Based Partial Application" = "Glucose Based Partial Application aktivieren"; + +/* Title for Integral Retrospective Correction toggle */ +"Enable Integral Retrospective Correction" = "Integral Retrospective Correction aktivieren"; + /* The action hint of the workout mode toggle button when disabled */ "Enables" = "Aktivieren"; @@ -507,6 +560,12 @@ /* The alert title for a resume error */ "Failed to Resume Insulin Delivery" = "Wiederaufnahme der Insulinabgabe fehlgeschlagen"; +/* No comment provided by engineer. */ +"FAVORITE FOODS" = "Favorisiertes Essen"; + +/* No comment provided by engineer. */ +"Favorite Foods" = "Favorisiertes Essen"; + /* Title of insulin model preset */ "Fiasp" = "Fiasp"; @@ -516,6 +575,10 @@ /* Secondary text for alerts disabled warning, which appears on the main status screen. */ "Fix now by turning Notifications, Critical Alerts and Time Sensitive Notifications ON." = "Behebe dies jetzt, indem Du Benachrichtigungen, kritische Alarme und zeitkritische Benachrichtigungen einschaltest."; +/* label for food type in favorite entry screen + Label for food type in favorite food entry */ +"Food Type" = "Essensart"; + /* The format string used to describe a finite workout targets duration */ "For %1$@" = "Für %1$@"; @@ -534,6 +597,10 @@ /* The title of the glucose and prediction graph */ "Glucose" = "Blutzucker"; +/* Title for glucose based partial application experiment description + Title of glucose based partial application experiment */ +"Glucose Based Partial Application" = "Glucose Based Partial Application"; + /* The error message when glucose data is too old to be used. (1: glucose data age in minutes) */ "Glucose data is %1$@ old" = "Blutzuckerdaten sind %1$@ alt"; @@ -559,6 +626,9 @@ /* Immediate Delivery status text */ "Immediate" = "Sofort"; +/* Algorithm Experiments description second paragraph. */ +"In future versions of Loop these experiments may change, end up as standard parts of the Loop Algorithm, or be removed from Loop entirely. Please follow along in the Loop Zulip chat to stay informed of possible changes to these features." = "In zukünftigen Versionen von Loop können sich diese Experimente ändern, als Standardbestandteile des Loop-Algorithmus enden oder vollständig aus Loop entfernt werden. Bitte folgen Sie dem Loop Zulip-Chat, um über mögliche Änderungen dieser Funktionen auf dem Laufenden zu bleiben."; + /* The title of a target alert action specifying an indefinitely long workout targets duration */ "Indefinitely" = "Unbegrenzt"; @@ -597,6 +667,13 @@ /* Insulin type label */ "Insulin Type" = "Insulintyp"; +/* Title for integral retrospective correction experiment description + Title of integral retrospective correction experiment */ +"Integral Retrospective Correction" = "Integrale retrospektive Korrektur"; + +/* Description of Integral Retrospective Correction toggle. */ +"Integral Retrospective Correction (IRC) is an extension of the standard Retrospective Correction (RC) algorithm component in Loop, which adjusts the forecast based on the history of discrepancies between predicted and actual glucose levels.\n\nIn contrast to RC, which looks at discrepancies over the last 30 minutes, with IRC, the history of discrepancies adds up over time. So continued positive discrepancies over time will result in increased dosing. If the discrepancies are negative over time, Loop will reduce dosing further." = "Integral Retrospective Correction (IRC) ist eine Erweiterung der standardmäßigen Retrospective Correction (RC)-Algorithmuskomponente in Loop, die die Prognose basierend auf der Historie der Abweichungen zwischen vorhergesagten und tatsächlichen Glukosewerten anpasst. \n\nIm Gegensatz zu RC, das die Abweichungen der letzten 30 Minuten betrachtet, summiert sich bei IRC der Verlauf der Abweichungen im Laufe der Zeit. Daher führen anhaltende positive Abweichungen im Laufe der Zeit zu einer erhöhten Dosierung. Wenn die Abweichungen im Laufe der Zeit negativ sind, reduziert Loop die Dosierung weiter."; + /* Description of an interrupted bolus dose entry (1: title for dose type, 2: value (? if no value) in bold, 3: programmed value (? if no value), 4: unit) */ "Interrupted %1$@: %2$@ of %3$@ %4$@" = "%1$@ unterbrochen: %2$@ von %3$@ %4$@"; @@ -658,6 +735,9 @@ /* The notification alert describing a long-lasting loop failure. The substitution parameter is the time interval since the last loop */ "Loop has not completed successfully in %@" = "Loop wurde nicht erfolgreich abgeschlossen seit %@"; +/* Description of Glucose Based Partial Application toggle. */ +"Loop normally gives 40% of your predicted insulin needs each dosing cycle.\n\nWhen the Glucose Based Partial Application experiment is enabled, Loop will vary the percentage of recommended bolus delivered each cycle with glucose level.\n\nNear correction range, it will use 20% (similar to Temp Basal), and gradually increase to a maximum of 80% at high glucose (200 mg/dL, 11.1 mmol/L).\n\nPlease be aware that during fast rising glucose, such as after an unannounced meal, this feature, combined with velocity and retrospective correction effects, may result in a larger dose than your ISF would call for." = "Loop liefert normalerweise in jedem Dosierungszyklus 40%1$ Ihres vorhergesagten Insulinbedarfs. \n\nWenn das Experiment Glucose Based Partial Application aktiviert ist, variiert Loop den Prozentsatz des empfohlenen Bolus, der in jedem Zyklus abgegeben wird, mit dem Glukosespiegel. \n\nIn der Nähe des Korrekturbereichs werden 20%2$ verwendet (ähnlich wie bei Temp Basal) und bei hohem Glukosewert (200 mg/dL, 11,1 mmol/L) allmählich auf ein Maximum von 80%3$ erhöht. \n\nBitte beachte, dass diese Funktion bei schnell ansteigendem Blutzucker, z. B. nach einer unangekündigten Mahlzeit, in Kombination mit Geschwindigkeits- und retrospektiven Korrektureffekten zu einer höheren Dosis führen kann, als Dein ISF erfordern würde."; + /* Description string for automatic bolus dosing strategy */ "Loop will automatically bolus when insulin needs are above scheduled basal, and will use temporary basal rates when needed to reduce insulin delivery below scheduled basal." = "Loop gibt automatisch einen Bolus ab, wenn der Insulinbedarf über der geplanten Basalrate liegt, und verwendet temporäre Basalraten, wenn dies erforderlich ist, um die Insulinabgabe unter die geplante Basalrate zu reduzieren."; @@ -707,12 +787,17 @@ /* Details for missing data error when momentum effects are missing */ "Momentum effects" = "Momentum-Effekte"; -/* Text for more info action on notification of upcoming profile expiration */ +/* Text for more info action on notification of upcoming profile expiration + Text for more info action on notification of upcoming TestFlight expiration */ "More Info" = "Weitere Info"; /* Label for button to mute all alerts */ "Mute All Alerts" = "Alle Alarme stummschalten"; +/* Label for name in favorite food entry + Label for name in favorite food entry screen */ +"Name" = "Name"; + /* Sensor state description for the non-valid state */ "Needs Attention" = "Erfordert Aufmerksamkeit"; @@ -777,6 +862,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "OK"; @@ -890,15 +976,24 @@ /* The title of the notification action to retry a bolus command */ "Retry" = "Wiederholen"; +/* No comment provided by engineer. */ +"Save" = "Speichern"; + /* Button text to save carbs and/or manual glucose entry and deliver a bolus */ "Save and Deliver" = "Speichern und Bolus abgeben"; +/* No comment provided by engineer. */ +"Save as favorite food" = "Als Favorit speichern"; + /* Button text to save carbs and/or manual glucose entry without a bolus */ "Save without Bolusing" = "Speichern ohne Bolusgabe"; /* Scheduled Delivery status text */ "Scheduled" = "Geplant"; +/* No comment provided by engineer. */ +"Selecting a favorite food in the carb entry screen automatically fills in the carb quantity, food type, and absorption time fields! Tap the add button below to create your first favorite food!" = "Wenn Du auf dem Kohlenhydrat-Eingabebildschirm ein Lieblingslebensmittel auswählst, werden automatisch die Felder Kohlenhydratmenge, Lebensmittelart und Absorptionszeit ausgefüllt! Tippe unten auf die Schaltfläche „Hinzufügen“, um Dein erstes Lieblingsessen zu erstellen!"; + /* The title of the services section in settings */ "Services" = "Dienste"; @@ -917,6 +1012,9 @@ /* Title of simple bolus view when displaying meal entry */ "Simple Meal Calculator" = "Einfacher Mahlzeitenrechner"; +/* subheadline of Favorite Foods */ +"Simplify Carb Entry" = "Vereinfachte KH Eingabe"; + /* Format fragment for a start time */ "since %@" = "seit %@"; @@ -960,6 +1058,9 @@ /* Message presented in the status row instructing the user to tap this row to stop a bolus */ "Tap to Stop" = "Stoppen"; +/* The alert body for unmute alert confirmation */ +"Tap Unmute to resume sound for your alerts and alarms." = "Tippe auf Stummschaltung aufheben, um den Ton für Deine Warnungen und Alarme wieder aufzunehmen."; + /* Alert message for a bolus too small validation error */ "The bolus amount entered is smaller than the minimum deliverable." = "Die eingegebene Bolusmenge ist kleiner als die Mindestabgabemenge."; diff --git a/Loop/es.lproj/Localizable.strings b/Loop/es.lproj/Localizable.strings index 728856649f..a1fd7ee4d9 100644 --- a/Loop/es.lproj/Localizable.strings +++ b/Loop/es.lproj/Localizable.strings @@ -692,7 +692,8 @@ /* Details for missing data error when momentum effects are missing */ "Momentum effects" = "Efectos de Momento"; -/* Text for more info action on notification of upcoming profile expiration */ +/* Text for more info action on notification of upcoming profile expiration + Text for more info action on notification of upcoming TestFlight expiration */ "More Info" = "Más Info"; /* Label for button to mute all alerts */ @@ -759,6 +760,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "OK"; diff --git a/Loop/fi.lproj/Localizable.strings b/Loop/fi.lproj/Localizable.strings index c05de83937..9ca9c0a007 100644 --- a/Loop/fi.lproj/Localizable.strings +++ b/Loop/fi.lproj/Localizable.strings @@ -543,7 +543,8 @@ /* Details for missing data error when momentum effects are missing */ "Momentum effects" = "Liikevaikutukset (momentum)"; -/* Text for more info action on notification of upcoming profile expiration */ +/* Text for more info action on notification of upcoming profile expiration + Text for more info action on notification of upcoming TestFlight expiration */ "More Info" = "Lisätietoa"; /* Sensor state description for the non-valid state */ @@ -583,6 +584,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "OK"; diff --git a/Loop/fr.lproj/Localizable.strings b/Loop/fr.lproj/Localizable.strings index d1631febe7..95225a94ed 100644 --- a/Loop/fr.lproj/Localizable.strings +++ b/Loop/fr.lproj/Localizable.strings @@ -683,7 +683,8 @@ /* Details for missing data error when momentum effects are missing */ "Momentum effects" = "Effets de momentum"; -/* Text for more info action on notification of upcoming profile expiration */ +/* Text for more info action on notification of upcoming profile expiration + Text for more info action on notification of upcoming TestFlight expiration */ "More Info" = "Plus d'informations"; /* Label for button to mute all alerts */ @@ -750,6 +751,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "OK"; diff --git a/Loop/he.lproj/Localizable.strings b/Loop/he.lproj/Localizable.strings index 6f65b8dea7..2ca902a1e0 100644 --- a/Loop/he.lproj/Localizable.strings +++ b/Loop/he.lproj/Localizable.strings @@ -476,7 +476,8 @@ /* Details for missing data error when momentum effects are missing */ "Momentum effects" = "השפעות מומנטום"; -/* Text for more info action on notification of upcoming profile expiration */ +/* Text for more info action on notification of upcoming profile expiration + Text for more info action on notification of upcoming TestFlight expiration */ "More Info" = "מידע נוסף"; /* Label for button to mute all alerts */ @@ -546,6 +547,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "אישור"; diff --git a/Loop/it.lproj/InfoPlist.strings b/Loop/it.lproj/InfoPlist.strings index 9e861d4c3a..f86acc5698 100644 --- a/Loop/it.lproj/InfoPlist.strings +++ b/Loop/it.lproj/InfoPlist.strings @@ -4,6 +4,9 @@ /* Bundle name */ "CFBundleName" = "$(PRODUCT_NAME)"; +/* Privacy - NFC Scan Usage Description */ +"NFCReaderUsageDescription" = "L'app utilizza NFC per l'accoppiamento con i dispositivi per il diabete."; + /* Privacy - Bluetooth Always Usage Description */ "NSBluetoothAlwaysUsageDescription" = "Il Bluetooth è utilizzato per comunicare con il microinfusore ed il sensore glicemico"; diff --git a/Loop/it.lproj/Localizable.strings b/Loop/it.lproj/Localizable.strings index 6e31a65780..0528bed0d4 100644 --- a/Loop/it.lproj/Localizable.strings +++ b/Loop/it.lproj/Localizable.strings @@ -4,6 +4,9 @@ /* Status row title for premeal override enabled (leading space is to separate from symbol) */ " Pre-meal Preset" = "Preimpostazioni del Pre-Pasto"; +/* remaining time in setting's profile expiration section */ +" remaining" = "Rimanente"; + /* Warning text for when Notifications or Critical Alerts Permissions is disabled */ " Safety Notifications are OFF" = "Le notifiche di sicurezza risultano spente"; @@ -92,11 +95,14 @@ Description of a bolus dose entry (1: title for dose type, 2: value (? if no value) in bold, 3: unit) */ "%1$@: %2$@ %3$@" = "%1$@ : %2$@ %3$@"; +/* No comment provided by engineer. */ +"⚠️" = "⚠️"; + /* Description of the prediction input effect for glucose momentum */ "15 min glucose regression coefficient (b₁), continued with decay over 30 min" = "Coefficiente di regressione del glucosio a 15 min (b₁), interpolato con il decadimento a 30 min."; /* Description of the prediction input effect for retrospective correction */ -"30 min comparison of glucose prediction vs actual, continued with decay over 60 min" = "30 min di confronto tra la previsione glicemica e quella attuale, proseguita con il degrado sino a 60 minuti"; +"30 min comparison of glucose prediction vs actual, continued with decay over 60 min" = "Confronto di 30 minuti tra la previsione del glucosio e quella effettiva, continuato con decadimento per 60 minuti"; /* Estimated remaining duration with a few seconds */ "A few seconds remaining" = "Pochi secondi rimanenti"; @@ -132,7 +138,7 @@ "Active Carbohydrates: %@" = "Carboidrati attivi: %@"; /* Title describing quantity of still-absorbing carbohydrates */ -"Active Carbs" = "Carb Attivi"; +"Active Carbs" = "Carboidrati Attivi"; /* The title of the Insulin On-Board graph */ "Active Insulin" = "Insulina attiva"; @@ -140,6 +146,9 @@ /* The string format describing active insulin. (1: localized insulin value description) */ "Active Insulin: %@" = "Insulina attiva: %@"; +/* No comment provided by engineer. */ +"Add a new favorite food" = "Aggiungi un nuovo cibo preferito"; + /* Title of the user activity for adding carbs */ "Add Carb Entry" = "Agg. Carb. Assunti"; @@ -168,9 +177,28 @@ Notification & Critical Alert Permissions screen title */ "Alert Permissions" = "Avvisi"; +/* Navigation title for algorithms experiments screen + The title of the Algorithm Experiments section in settings */ +"Algorithm Experiments" = "Esperimenti sugli algoritmi"; + +/* Algorithm Experiments description. */ +"Algorithm Experiments are optional modifications to the Loop Algorithm. These modifications are less tested than the standard Loop Algorithm, so please use carefully." = "Gli esperimenti sull'algoritmo sono modifiche opzionali all'algoritmo del loop. Queste modifiche sono meno testate rispetto all'algoritmo Loop standard, quindi utilizzale con Attenzione."; + /* The title of the section containing algorithm settings */ "Algorithm Settings" = "Impostazioni Algoritmo"; +/* Warning text for when alerts are muted */ +"All Alerts Muted" = "Tutti gli avvisi silenziati"; + +/* Label for when mute alert will end */ +"All alerts muted until" = "silenzia tutti gli avvisi fino"; + +/* No comment provided by engineer. */ +"All Favorites" = "Tutti i preferiti"; + +/* Label for carb quantity entry row on carb entry screen */ +"Amount Consumed" = "Quantità consumata"; + /* The title of the Amplitude service */ "Amplitude" = "Amplitude"; @@ -195,6 +223,9 @@ /* The title of the nightscout API secret credential */ "API Secret" = "Chiave personale API"; +/* Settings app profile section */ +"App Profile" = "Profilo App"; + /* Action sheet confirmation message for pump history deletion */ "Are you sure you want to delete all history entries?" = "Sei sicuro di voler eliminare tutte le voci della cronologia?"; @@ -210,6 +241,9 @@ /* Confirmation message for deleting a CGM */ "Are you sure you want to delete this CGM?" = "Sei sicuro di voler eliminare questo CGM?"; +/* No comment provided by engineer. */ +"Are you sure you want to delete this food?" = "Sei sicuro di voler cancellare questo cibo?"; + /* Confirmation message for deleting a service */ "Are you sure you want to delete this service?" = "Sei sicuro di voler eliminare questo servizio?"; @@ -297,6 +331,9 @@ /* Description of the prediction input effect for carbohydrates. (1: The glucose unit string) */ "Carbs Absorbed (g) ÷ Carb Ratio (g/U) × Insulin Sensitivity (%1$@/U)" = "Carboidrati Assorbiti ÷ Rapporto Carboidrati (gr/U) × Sensibilità Insulinica (%1$@/U)"; +/* No comment provided by engineer. */ +"Caution" = "Attenzione"; + /* The notification alert describing a low pump battery */ "Change the pump battery immediately" = "Cambiare immediatamente la batteria del microinfusore"; @@ -307,7 +344,7 @@ "Check settings" = "Controllare le impostazioni"; /* Recovery suggestion when reservoir data is missing */ -"Check that your pump is in range" = "Controlllare che il microinfusore si trovi vicino"; +"Check that your pump is in range" = "Controllare che il microinfusore si trovi vicino"; /* Recovery suggestion when glucose data is missing */ "Check your CGM data source" = "Controllare la sorgente dati del sensore"; @@ -318,6 +355,9 @@ /* Carb entry section footer text explaining absorption time */ "Choose a longer absorption time for larger meals, or those containing fats and proteins. This is only guidance to the algorithm and need not be exact." = "Scegli un tempo di assorbimento piu lungo per i pasti piu grandi o quelli contenenti grassi e proteine. Questa e solo una guida all’algoritmo e non e necessario che sia esatta."; +/* No comment provided by engineer. */ +"Choose Favorite:" = "Scegli il preferito:"; + /* Button title to close view The button label of the action used to dismiss the unsafe notification permission alert */ "Close" = "Chiudi"; @@ -362,6 +402,9 @@ The title text for the glucose target range schedule */ "Correction Range" = "Intervallo Glicemico"; +/* Format string for title of reset loop alert. (1: App name) */ +"Could Not Restart %1$@" = "Impossibile riavviare %1$@"; + /* Critical Alerts Status text */ "Critical Alerts" = "Avvisi critici"; @@ -395,6 +438,9 @@ /* No comment provided by engineer. */ "Delete" = "Cancella"; +/* No comment provided by engineer. */ +"Delete “%@”?" = "Cancellare \"%@\" ?"; + /* The title of the button to remove the credentials for a service */ "Delete Account" = "Cancella Account"; @@ -404,6 +450,9 @@ /* Button title to delete CGM */ "Delete CGM" = "Elimina CGM"; +/* No comment provided by engineer. */ +"Delete Food" = "Cancella cibo"; + /* Button title to delete a service */ "Delete Service" = "Elimina Servizio"; @@ -444,9 +493,18 @@ /* The title of the Dosing Strategy section in settings */ "Dosing Strategy" = "Strategia di dosaggio"; +/* Override error description: duration exceed max (1: max duration in hours). */ +"Duration exceeds: %1$.1f hours" = "La durata supera: %1$.1f ore"; + /* Message to the user to enable bluetooth */ "Enable\nBluetooth" = "Abilita\n Bluetooth"; +/* Title for Glucose Based Partial Application toggle */ +"Enable Glucose Based Partial Application" = "Abilita l'applicazione parziale basata sul glucosio"; + +/* Title for Integral Retrospective Correction toggle */ +"Enable Integral Retrospective Correction" = "Abilita Correzione Retrospettiva Integrale"; + /* The action hint of the workout mode toggle button when disabled */ "Enables" = "Abilita"; @@ -498,12 +556,18 @@ /* The alert title for a resume error */ "Failed to Resume Insulin Delivery" = "Impossibile riprendere l'erogazione dell'insulina"; +/* No comment provided by engineer. */ +"FAVORITE FOODS" = "CIBI SALVATI"; + /* Title of insulin model preset */ "Fiasp" = "Fiasp"; /* Label for manual glucose entry row on bolus screen */ "Fingerstick Glucose" = "Glicemia da dito"; +/* Secondary text for alerts disabled warning, which appears on the main status screen. */ +"Fix now by turning Notifications, Critical Alerts and Time Sensitive Notifications ON." = "Risolvilo ora attivando Notifiche, Avvisi critici e Notifiche urgenti."; + /* The format string used to describe a finite workout targets duration */ "For %1$@" = "Per %1$@"; @@ -522,6 +586,10 @@ /* The title of the glucose and prediction graph */ "Glucose" = "Glicemia"; +/* Title for glucose based partial application experiment description + Title of glucose based partial application experiment */ +"Glucose Based Partial Application" = "Glucose Based Partial Application (GBPA)"; + /* The error message when glucose data is too old to be used. (1: glucose data age in minutes) */ "Glucose data is %1$@ old" = "I dati sulla glicemia sono %1$@ vecchi"; @@ -531,6 +599,9 @@ /* Alert title when glucose data returns while on bolus screen */ "Glucose Data Now Available" = "Dati Glicemie ora disponibili"; +/* Description of the prediction input effect for suspension of insulin delivery */ +"Glucose effect of suspending insulin delivery" = "Effetto sulla glicemia della sospensione della somministrazione di insulina"; + /* Alert title for a manual glucose entry out of range error Title for bolus screen warning when glucose entry is out of range */ "Glucose Entry Out of Range" = "Glicemia inserita fuori dall'intervallo"; @@ -541,9 +612,15 @@ /* Details for configuration error when glucose target range schedule is missing */ "Glucose Target Range Schedule" = "Programma degli intervalli degli obiettivi glicemici"; +/* The title text for how to update */ +"How to update (LoopDocs)" = "Come Aggiornare (LoopDocs)"; + /* Immediate Delivery status text */ "Immediate" = "Immediato"; +/* Algorithm Experiments description second paragraph. */ +"In future versions of Loop these experiments may change, end up as standard parts of the Loop Algorithm, or be removed from Loop entirely. Please follow along in the Loop Zulip chat to stay informed of possible changes to these features." = "Nelle versioni future di Loop questi esperimenti potrebbero cambiare, diventare parti standard dell'algoritmo Loop o essere rimossi completamente da Loop. Segui la chat di Loop Zulip per rimanere informato su possibili modifiche a queste funzionalità."; + /* The title of a target alert action specifying an indefinitely long workout targets duration */ "Indefinitely" = "A tempo indeterminato"; @@ -582,9 +659,22 @@ /* Insulin type label */ "Insulin Type" = "Tipo d'insulina"; +/* Title for integral retrospective correction experiment description + Title of integral retrospective correction experiment */ +"Integral Retrospective Correction" = "Correzione retrospettiva Integrale"; + +/* Description of Integral Retrospective Correction toggle. */ +"Integral Retrospective Correction (IRC) is an extension of the standard Retrospective Correction (RC) algorithm component in Loop, which adjusts the forecast based on the history of discrepancies between predicted and actual glucose levels.\n\nIn contrast to RC, which looks at discrepancies over the last 30 minutes, with IRC, the history of discrepancies adds up over time. So continued positive discrepancies over time will result in increased dosing. If the discrepancies are negative over time, Loop will reduce dosing further." = "La Correzione Retrospettiva Integrale (IRC) è un'estensione del componente standard dell'algoritmo di correzione retrospettiva (RC) in Loop, che regola la previsione in base alla cronologia delle discrepanze tra i livelli di glucosio previsti e quelli effettivi. \n\nA differenza di RC, che esamina le discrepanze negli ultimi 30 minuti, con IRC la cronologia delle discrepanze si accumula nel tempo. Pertanto, continue discrepanze positive nel tempo comporteranno un aumento del dosaggio. Se le discrepanze diventano negative nel tempo, Loop ridurrà ulteriormente il dosaggio."; + /* Description of an interrupted bolus dose entry (1: title for dose type, 2: value (? if no value) in bold, 3: programmed value (? if no value), 4: unit) */ "Interrupted %1$@: %2$@ of %3$@ %4$@" = "Interrotto %1$@ : %2$@ di %3$@ %4$@"; +/* Carb error description: invalid absorption time. (1: Input duration in hours). */ +"Invalid absorption time: %1$@ hours" = "Tempo di assorbimento non valido: %d ore"; + +/* Bolus error description: invalid bolus amount. */ +"Invalid Bolus Amount" = "Quantità di bolo non valida"; + /* Carb error description: invalid carb amount. */ "Invalid carb amount" = "Quantità di carboidrati non valida"; @@ -600,6 +690,9 @@ /* The title text for the issue report cell */ "Issue Report" = "Report dei problemi"; +/* The notification description for a meal that was possibly not logged in Loop. */ +"It looks like you may not have logged a meal you ate. Tap to log it now." = "Sembra che non sia stato registrato un pasto consumato. Toccare per registrarlo ora."; + /* Title of the warning shown when a large meal was entered */ "Large Meal Entered" = "Pasto abbondante inserito"; @@ -631,9 +724,15 @@ /* Bluetooth unavailable alert body. */ "Loop has detected an issue with your Bluetooth settings, and will not work successfully until Bluetooth is enabled. You will not receive glucose readings, or be able to bolus." = "Loop ha rilevato un problema con le tue impostazioni Bluetooth e non funzionerà correttamente finché il Bluetooth non sarà abilitato. Non riceverai letture glicemiche né potrai eseguire il bolo."; +/* Warning displayed when user is adding a meal from an missed meal notification */ +"Loop has detected an missed meal and estimated its size. Edit the carb amount to match the amount of any carbs you may have eaten." = "Loop ha rilevato un pasto saltato e ne ha stimato le dimensioni. Modifica la quantità di carboidrati in modo che corrisponda alla quantità di carboidrati che potresti aver mangiato."; + /* The notification alert describing a long-lasting loop failure. The substitution parameter is the time interval since the last loop */ "Loop has not completed successfully in %@" = "Loop non ha funzionato correttamente per %@"; +/* Description of Glucose Based Partial Application toggle. */ +"Loop normally gives 40% of your predicted insulin needs each dosing cycle.\n\nWhen the Glucose Based Partial Application experiment is enabled, Loop will vary the percentage of recommended bolus delivered each cycle with glucose level.\n\nNear correction range, it will use 20% (similar to Temp Basal), and gradually increase to a maximum of 80% at high glucose (200 mg/dL, 11.1 mmol/L).\n\nPlease be aware that during fast rising glucose, such as after an unannounced meal, this feature, combined with velocity and retrospective correction effects, may result in a larger dose than your ISF would call for." = "Loop normalmente fornisce il 40%1$ del fabbisogno di insulina previsto per ogni ciclo di dosaggio. \n\n Quando l'esperimento di Applicazione Parziale Basata sul Glucosio (GBPA) è abilitato, il Loop varierà la percentuale del bolo consigliato erogato ad ogni ciclo con il livello di glucosio. \n\nVicino all'intervallo di correzione, utilizzerà il 20%2$ (simile alla basale temporanea) e aumenterà gradualmente fino a un massimo dell'80%3$ in caso di glicemia elevata (200 mg/dl, 11,1 mmol/l). \n\nTieni presente che durante un rapido aumento della glicemia, ad esempio dopo un pasto imprevisto, questa caratteristica, combinata con la velocità e gli effetti di correzione retrospettiva, può comportare una dose maggiore di quella richiesta dall'FSI."; + /* Description string for automatic bolus dosing strategy */ "Loop will automatically bolus when insulin needs are above scheduled basal, and will use temporary basal rates when needed to reduce insulin delivery below scheduled basal." = "Loop eseguirà automaticamente il bolo quando il fabbisogno d'insulina è superiore alla basale programmata e utilizzerà velocità basali temporanee quando necessario per ridurre l'erogazione d'insulina al di sotto della basale programmata."; @@ -668,6 +767,9 @@ /* The short unit display string for milligrams of glucose per decilter */ "mg/dL" = "mg/dL"; +/* Title for missed meal notifications toggle */ +"Missed Meal Notifications" = "Notifiche di pasti mancati"; + /* The error message for missing data. (1: missing data details) */ "Missing data: %1$@" = "Dati mancanti: %1$@"; @@ -680,18 +782,28 @@ /* Details for missing data error when momentum effects are missing */ "Momentum effects" = "Effetto glicemico attuale"; -/* Text for more info action on notification of upcoming profile expiration */ +/* Text for more info action on notification of upcoming profile expiration + Text for more info action on notification of upcoming TestFlight expiration */ "More Info" = "Piu info"; /* Label for button to mute all alerts */ "Mute All Alerts" = "Disattiva tutti gli avvisi"; +/* Title for mute alert duration selection action sheet */ +"Mute All Alerts Temporarily" = "silenzia tutti gli avvisi temporaneamente"; + /* Sensor state description for the non-valid state */ "Needs Attention" = "Esige Attenzione"; +/* Override error description: negative duration error. */ +"Negative duration not allowed" = "Durata negativa non consentita"; + /* The title of the Nightscout service */ "Nightscout" = "Nightscout"; +/* Message for mute alert duration selection action sheet */ +"No alerts or alarms will sound while muted. Select how long you would you like to mute for." = "Nessun avviso o allarme suonerà quando l'audio è disattivato. Seleziona per quanto tempo desideri disattivare l'audio."; + /* Title for bolus screen notice when no bolus is recommended Title for bolus screen warning when glucose is below suspend threshold, and a bolus is not recommended Title for bolus screen warning when no bolus is recommended */ @@ -747,6 +859,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "OK"; @@ -756,6 +869,9 @@ /* The title text for the override presets */ "Override Presets" = "Programma Alternativo"; +/* The notification title for a meal that was possibly not logged in Loop. */ +"Possible Missed Meal" = "Possibile pasto saltato"; + /* The label of the pre-meal mode toggle button */ "Pre-Meal Targets" = "Obiettivo Pre-Pasto"; @@ -774,9 +890,18 @@ /* Format string describing retrospective glucose prediction comparison. (1: Predicted glucose)(2: Actual glucose)(3: difference) */ "Predicted: %1$@\nActual: %2$@ (%3$@)" = "Previsto: %1$@\nEffettivo: %2$@ (%3$@)"; +/* Format string describing integral retrospective correction. (1: Integral glucose effect)(2: Total glucose effect) */ +"prediction-description-integral-retrospective-correction" = "previsione-descrizione-integrale-retrospettiva-correzione"; + /* Preparing critical event log text */ "Preparing Critical Event Logs" = "Lista degli eventi critici in preparazione"; +/* Settings App Profile expiration view */ +"Profile Expiration" = "Scadenza Profilo"; + +/* Time that profile expires */ +"Profile expires " = "Profilo scaduto"; + /* The title for notification of upcoming profile expiration */ "Profile Expires Soon" = "Il profilo scadra' presto"; @@ -851,15 +976,24 @@ /* The title of the notification action to retry a bolus command */ "Retry" = "Riprova"; +/* No comment provided by engineer. */ +"Save" = "Salva"; + /* Button text to save carbs and/or manual glucose entry and deliver a bolus */ "Save and Deliver" = "Salva e Invia"; +/* No comment provided by engineer. */ +"Save as favorite food" = "Salva come cibo preferito"; + /* Button text to save carbs and/or manual glucose entry without a bolus */ "Save without Bolusing" = "Salva senza bolo"; /* Scheduled Delivery status text */ "Scheduled" = "Programmato"; +/* No comment provided by engineer. */ +"Selecting a favorite food in the carb entry screen automatically fills in the carb quantity, food type, and absorption time fields! Tap the add button below to create your first favorite food!" = "Selezionando un alimento preferito nella schermata di immissione dei carboidrati si riempiono automaticamente i campi relativi alla quantità di carboidrati, al tipo di alimento e al tempo di assorbimento! Tocca il pulsante Aggiungi qui sotto per creare il tuo primo cibo preferito!"; + /* The title of the services section in settings */ "Services" = "Servizi"; @@ -887,6 +1021,9 @@ /* Software update button link text */ "Software Update" = "Aggiornamento software"; +/* Carb error description: invalid start time is out of range. */ +"Start time is out of range: %@" = "L'ora di inizio non rientra nell'intervallo: %@"; + /* The format for the description of a temporary override start date */ "starting at %@" = "inizia a %@"; @@ -900,6 +1037,9 @@ /* The title text in settings */ "Suspend Threshold" = "Sospendi Soglia"; +/* Title of the prediction input effect for suspension of insulin delivery */ +"Suspension of Insulin Delivery" = "Sospensione della somministrazione di insulina"; + /* Descriptive text for button to add CGM device */ "Tap here to set up a CGM" = "Premi per impostare un CGM"; @@ -918,6 +1058,12 @@ /* Message presented in the status row instructing the user to tap this row to stop a bolus */ "Tap to Stop" = "Interrompi"; +/* Label for button to unmute all alerts */ +"Tap to Unmute Alerts" = "Clicca per riattivare gli avvisi"; + +/* The alert body for unmute alert confirmation */ +"Tap Unmute to resume sound for your alerts and alarms." = "Tocca Riattiva per ripristinare l'audio per gli avvisi e le sveglie."; + /* Alert message for a bolus too small validation error */ "The bolus amount entered is smaller than the minimum deliverable." = "La quantità di bolo immessa è inferiore alla quantità minima erogabile."; @@ -948,6 +1094,9 @@ /* Title text for button to Therapy Settings */ "Therapy Settings" = "Impostazioni Terapia"; +/* String shown when glucose based partial application cannot be enabled because dosing strategy is not set to Automatic Bolus */ +"This option only applies when Loop's Dosing Strategy is set to Automatic Bolus." = "Questa opzione si applica solo quando la strategia di dosaggio di Loop è impostata su Bolo automatico."; + /* Time Sensitive Status text */ "Time Sensitive Notifications" = "Notifiche a tempo"; @@ -984,9 +1133,27 @@ /* The error message displayed for unknown errors. (1: unknown error) */ "Unknown Error: %1$@" = "Errore sconosciuto: %1$@"; +/* Override error description: unknown preset (1: preset name). */ +"Unknown preset: %1$@" = "Preimpostazione sconosciuta: %1$@"; + +/* Unknown amount of time in settings' profile expiration section */ +"Unknown time" = "Ora sconosciuta"; + +/* The title of the action used to unmute alerts */ +"Unmute" = "Riattiva"; + +/* The alert title for unmute alert confirmation */ +"Unmute Alerts?" = "Attivare gli avvisi?"; + +/* Error message when a service can't be found to handle a push notification. (1: Service Identifier) */ +"Unsupported Notification Service: %1$@" = "Servizio di notifica non supportato: %1$@"; + /* The format for the description of a temporary override end date */ "until %@" = "fino a %@"; +/* indication of when alerts will be unmuted (1: time when alerts unmute) */ +"Until %1$@" = "Fino al %1$@"; + /* The title of a target alert action specifying pre-meal targets duration for 1 hour or until the user enters carbs (whichever comes first). */ "Until I enter carbs" = "Fino a quando non inserisco carboidrati"; @@ -1014,9 +1181,15 @@ /* Explanation of suspend threshold */ "When current or forecasted glucose is below the suspend threshold, Loop will not recommend a bolus, and will always recommend a temporary basal rate of 0 units per hour." = "Quando la glicemia attuale o prevista è sotto la soglia di sospensione, Loop non consiglia un bolo, e raccomanda una velocità basale temporanea di 0 unità per ora."; +/* Description of missed meal notifications. */ +"When enabled, Loop can notify you when it detects a meal that wasn't logged." = "Se abilitato, Loop può avvisarti quando rileva un pasto che non è stato registrato."; + /* No comment provided by engineer. */ "When out of Closed Loop mode, the app uses a simplified bolus calculator like a typical pump." = "Quando non è in modalità ciclo chiuso, l'applicazione utilizza un calcolatore di bolo semplificato come un tipico microinfusore."; +/* Format string for message of reset loop alert. (1: App name) (2: error description) */ +"While trying to restart %1$@ an error occured.\n\n%2$@" = "Durante il tentativo di riavviare %1$@ si è verificato un errore.\n\n%2$@"; + /* The label of the workout mode toggle button */ "Workout Targets" = "Obiettivi di allenamento"; diff --git a/Loop/ja.lproj/Localizable.strings b/Loop/ja.lproj/Localizable.strings index 0971d5d384..0e443836e9 100644 --- a/Loop/ja.lproj/Localizable.strings +++ b/Loop/ja.lproj/Localizable.strings @@ -331,7 +331,8 @@ /* Details for missing data error when momentum effects are missing */ "Momentum effects" = "モメンタム効果"; -/* Text for more info action on notification of upcoming profile expiration */ +/* Text for more info action on notification of upcoming profile expiration + Text for more info action on notification of upcoming TestFlight expiration */ "More Info" = "詳細"; /* Sensor state description for the non-valid state */ @@ -348,6 +349,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "OK"; diff --git a/Loop/nb.lproj/InfoPlist.strings b/Loop/nb.lproj/InfoPlist.strings index 4e6970dff3..13ca15ffc2 100644 --- a/Loop/nb.lproj/InfoPlist.strings +++ b/Loop/nb.lproj/InfoPlist.strings @@ -4,6 +4,9 @@ /* Bundle name */ "CFBundleName" = "$(PRODUCT_NAME)"; +/* Privacy - NFC Scan Usage Description */ +"NFCReaderUsageDescription" = "Appen bruker NFC til å koble seg sammen med diabetesenheter."; + /* Privacy - Bluetooth Always Usage Description */ "NSBluetoothAlwaysUsageDescription" = "Bluetooth brukes til å kommunisere med insulinpumpe og kontinuerlige glukosemonitorer."; diff --git a/Loop/nb.lproj/Localizable.strings b/Loop/nb.lproj/Localizable.strings index 3a7e5ce6b0..4284ceb32d 100644 --- a/Loop/nb.lproj/Localizable.strings +++ b/Loop/nb.lproj/Localizable.strings @@ -2,7 +2,7 @@ " (pending: %@)" = "(venter: %@ )"; /* Status row title for premeal override enabled (leading space is to separate from symbol) */ -" Pre-meal Preset" = " Forhåndsinnstilling før måltid"; +" Pre-meal Preset" = "Forhåndsinnstilling før måltid"; /* remaining time in setting's profile expiration section */ " remaining" = "gjenstående"; @@ -95,6 +95,9 @@ Description of a bolus dose entry (1: title for dose type, 2: value (? if no value) in bold, 3: unit) */ "%1$@: %2$@ %3$@" = "%1$@ : %2$@ %3$@"; +/* No comment provided by engineer. */ +"⚠️" = "⚠️"; + /* Description of the prediction input effect for glucose momentum */ "15 min glucose regression coefficient (b₁), continued with decay over 30 min" = "15 minutters glukose-regresjonskoeffisient (b1), fortsatt med nedbrytning over 30 minutter."; @@ -143,6 +146,9 @@ /* The string format describing active insulin. (1: localized insulin value description) */ "Active Insulin: %@" = "Aktivt insulin: %@"; +/* No comment provided by engineer. */ +"Add a new favorite food" = "Legg til en ny favorittmat"; + /* Title of the user activity for adding carbs */ "Add Carb Entry" = "Legg til karbohydrater"; @@ -171,9 +177,28 @@ Notification & Critical Alert Permissions screen title */ "Alert Permissions" = "Varslingsinnstillinger"; +/* Navigation title for algorithms experiments screen + The title of the Algorithm Experiments section in settings */ +"Algorithm Experiments" = "Algoritmeeksperimenter"; + +/* Algorithm Experiments description. */ +"Algorithm Experiments are optional modifications to the Loop Algorithm. These modifications are less tested than the standard Loop Algorithm, so please use carefully." = "Algoritmeeksperimenter er valgfrie modifikasjoner til Loop-algoritmen. Disse modifikasjonene er mindre testet enn standard Loop-algoritmen, så vær vennlig å bruke dem med forsiktighet."; + /* The title of the section containing algorithm settings */ "Algorithm Settings" = "Algoritmeinnstillinger"; +/* Warning text for when alerts are muted */ +"All Alerts Muted" = "Alle varsler er dempet"; + +/* Label for when mute alert will end */ +"All alerts muted until" = "Alle varsler er dempet inntil"; + +/* No comment provided by engineer. */ +"All Favorites" = "Alle favoritter"; + +/* Label for carb quantity entry row on carb entry screen */ +"Amount Consumed" = "Mengde karbohydrater\n(Mengde inntatt)"; + /* The title of the Amplitude service */ "Amplitude" = "Amplitude"; @@ -216,6 +241,9 @@ /* Confirmation message for deleting a CGM */ "Are you sure you want to delete this CGM?" = "Er du sikker på at du vil slette denne CGM?"; +/* No comment provided by engineer. */ +"Are you sure you want to delete this food?" = "Er du sikker på at du vil slette denne maten?"; + /* Confirmation message for deleting a service */ "Are you sure you want to delete this service?" = "Er du sikker på at du vil slette denne tjenesten?"; @@ -303,6 +331,9 @@ /* Description of the prediction input effect for carbohydrates. (1: The glucose unit string) */ "Carbs Absorbed (g) ÷ Carb Ratio (g/U) × Insulin Sensitivity (%1$@/U)" = "Absorberte karbohydrater (g) ÷ Karbforhold (g/E) × insulinfølsomhet ( %1$@ /E)"; +/* No comment provided by engineer. */ +"Caution" = "Forsiktig"; + /* The notification alert describing a low pump battery */ "Change the pump battery immediately" = "Skift pumpebatteriet umiddelbart"; @@ -324,6 +355,9 @@ /* Carb entry section footer text explaining absorption time */ "Choose a longer absorption time for larger meals, or those containing fats and proteins. This is only guidance to the algorithm and need not be exact." = "Velg lengre absorpsjonstid for større måltider, eller de som inneholder fett og proteiner. Dette er kun veiledning til algoritmen og trenger ikke være nøyaktig."; +/* No comment provided by engineer. */ +"Choose Favorite:" = "Velg favoritt:"; + /* Button title to close view The button label of the action used to dismiss the unsafe notification permission alert */ "Close" = "Lukk"; @@ -368,6 +402,9 @@ The title text for the glucose target range schedule */ "Correction Range" = "Korreksjonsområde"; +/* Format string for title of reset loop alert. (1: App name) */ +"Could Not Restart %1$@" = "Kunne ikke starte %1$@ på nytt"; + /* Critical Alerts Status text */ "Critical Alerts" = "Kritiske varsler"; @@ -401,6 +438,9 @@ /* No comment provided by engineer. */ "Delete" = "Slett"; +/* No comment provided by engineer. */ +"Delete “%@”?" = "Slette \"%@\"?"; + /* The title of the button to remove the credentials for a service */ "Delete Account" = "Slett Konto"; @@ -410,6 +450,9 @@ /* Button title to delete CGM */ "Delete CGM" = "Slett CGM"; +/* No comment provided by engineer. */ +"Delete Food" = "Slett mat"; + /* Button title to delete a service */ "Delete Service" = "Slett tjeneste"; @@ -456,6 +499,12 @@ /* Message to the user to enable bluetooth */ "Enable\nBluetooth" = "Aktiver blåtann"; +/* Title for Glucose Based Partial Application toggle */ +"Enable Glucose Based Partial Application" = "Aktiver delvis anvendelse basert på glukose"; + +/* Title for Integral Retrospective Correction toggle */ +"Enable Integral Retrospective Correction" = "Aktivere integrert retrospektiv korrigering"; + /* The action hint of the workout mode toggle button when disabled */ "Enables" = "Aktiverer"; @@ -487,7 +536,7 @@ "Event History" = "Hendelseshistorie"; /* The subtitle format describing eventual glucose. (1: localized glucose value description) */ -"Eventually %@" = "Til slutt %@"; +"Eventually %@" = "Omsider %@"; /* Bolus error description: bolus exceeds maximum bolus in settings. */ "Exceeds maximum allowed bolus in settings" = "Overskrider maksimalt tillatt bolus i innstillingene"; @@ -507,6 +556,9 @@ /* The alert title for a resume error */ "Failed to Resume Insulin Delivery" = "Kunne ikke gjenoppta insulinlevering"; +/* No comment provided by engineer. */ +"FAVORITE FOODS" = "FAVORITTMAT"; + /* Title of insulin model preset */ "Fiasp" = "Fiasp"; @@ -534,6 +586,10 @@ /* The title of the glucose and prediction graph */ "Glucose" = "Blodsukker"; +/* Title for glucose based partial application experiment description + Title of glucose based partial application experiment */ +"Glucose Based Partial Application" = "Partiell anvendelse basert på glukosenivå"; + /* The error message when glucose data is too old to be used. (1: glucose data age in minutes) */ "Glucose data is %1$@ old" = "Blodsukkerdata er %1$@ gammel"; @@ -543,6 +599,9 @@ /* Alert title when glucose data returns while on bolus screen */ "Glucose Data Now Available" = "Blodsukkerdata er utilgjengelig"; +/* Description of the prediction input effect for suspension of insulin delivery */ +"Glucose effect of suspending insulin delivery" = "Glukoseeffekt av å suspendere insulintilførsel"; + /* Alert title for a manual glucose entry out of range error Title for bolus screen warning when glucose entry is out of range */ "Glucose Entry Out of Range" = "Blodsukkerdata er utenfor intervallet"; @@ -559,6 +618,9 @@ /* Immediate Delivery status text */ "Immediate" = "Umiddelbar"; +/* Algorithm Experiments description second paragraph. */ +"In future versions of Loop these experiments may change, end up as standard parts of the Loop Algorithm, or be removed from Loop entirely. Please follow along in the Loop Zulip chat to stay informed of possible changes to these features." = "I fremtidige versjoner av Loop kan disse eksperimentene endres, ende opp som standarddeler av Loop-algoritmen eller fjernes helt fra Loop. Følg med i Loop Zulip-chatten for å holde deg informert om eventuelle endringer i disse funksjonene."; + /* The title of a target alert action specifying an indefinitely long workout targets duration */ "Indefinitely" = "På ubestemt tid"; @@ -597,9 +659,19 @@ /* Insulin type label */ "Insulin Type" = "Insulintype"; +/* Title for integral retrospective correction experiment description + Title of integral retrospective correction experiment */ +"Integral Retrospective Correction" = "Integrert retrospektiv korreksjon"; + +/* Description of Integral Retrospective Correction toggle. */ +"Integral Retrospective Correction (IRC) is an extension of the standard Retrospective Correction (RC) algorithm component in Loop, which adjusts the forecast based on the history of discrepancies between predicted and actual glucose levels.\n\nIn contrast to RC, which looks at discrepancies over the last 30 minutes, with IRC, the history of discrepancies adds up over time. So continued positive discrepancies over time will result in increased dosing. If the discrepancies are negative over time, Loop will reduce dosing further." = "Integral Retrospective Correction (IRC) er en utvidelse av standardalgoritmekomponenten Retrospective Correction (RC) i Loop, som justerer prognosen basert på historikken for avvik mellom forventede og faktiske glukosenivåer.\n\nI motsetning til RC, som ser på avvik i løpet av de siste 30 minuttene, summerer IRC avvikene over tid. Fortsatte positive avvik over tid vil derfor føre til økt dosering. Hvis avvikene er negative over tid, vil Loop redusere doseringen ytterligere."; + /* Description of an interrupted bolus dose entry (1: title for dose type, 2: value (? if no value) in bold, 3: programmed value (? if no value), 4: unit) */ "Interrupted %1$@: %2$@ of %3$@ %4$@" = "Avbrutt %1$@: %2$@ av %3$@ %4$@"; +/* Carb error description: invalid absorption time. (1: Input duration in hours). */ +"Invalid absorption time: %1$@ hours" = "Ugyldig absorpsjonstid: %1$@ timer"; + /* Bolus error description: invalid bolus amount. */ "Invalid Bolus Amount" = "Ugyldig bolusmengde"; @@ -658,6 +730,9 @@ /* The notification alert describing a long-lasting loop failure. The substitution parameter is the time interval since the last loop */ "Loop has not completed successfully in %@" = "Loop har ikke fullført i %@"; +/* Description of Glucose Based Partial Application toggle. */ +"Loop normally gives 40% of your predicted insulin needs each dosing cycle.\n\nWhen the Glucose Based Partial Application experiment is enabled, Loop will vary the percentage of recommended bolus delivered each cycle with glucose level.\n\nNear correction range, it will use 20% (similar to Temp Basal), and gradually increase to a maximum of 80% at high glucose (200 mg/dL, 11.1 mmol/L).\n\nPlease be aware that during fast rising glucose, such as after an unannounced meal, this feature, combined with velocity and retrospective correction effects, may result in a larger dose than your ISF would call for." = "Loop gir normalt 40 %1$ av det forventede insulinbehovet i hver doseringssyklus.\n\nNår eksperimentet Glukosebasert delvis tilførsel er aktivert, vil Loop variere prosentandelen av anbefalt bolus som tilføres hver syklus med glukosenivået.\n\nI nærheten av korreksjonsområdet bruker den 20 %2$ (i likhet med Temp Basal), og øker gradvis til maksimalt 80 %3$ ved høyt glukosenivå (200 mg/dL, 11,1 mmol/L).\n\nVær oppmerksom på at når glukosenivået stiger raskt, f.eks. etter et uanmeldt måltid, kan denne funksjonen, kombinert med hastighets- og retrospektive korreksjonseffekter, resultere i en større dose enn det ISF skulle tilsi."; + /* Description string for automatic bolus dosing strategy */ "Loop will automatically bolus when insulin needs are above scheduled basal, and will use temporary basal rates when needed to reduce insulin delivery below scheduled basal." = "Lopp vil sette bolus når insulinbehovet er over planlagt basal, og vil bruke midlertidige basale rater når det er nødvendig for å redusere insulintilførselen under planlagt basal"; @@ -707,12 +782,16 @@ /* Details for missing data error when momentum effects are missing */ "Momentum effects" = "Momentum effekter"; -/* Text for more info action on notification of upcoming profile expiration */ +/* Text for more info action on notification of upcoming profile expiration + Text for more info action on notification of upcoming TestFlight expiration */ "More Info" = "Mer info"; /* Label for button to mute all alerts */ "Mute All Alerts" = "Demp alle varsler"; +/* Title for mute alert duration selection action sheet */ +"Mute All Alerts Temporarily" = "Slå av alle varsler midlertidig"; + /* Sensor state description for the non-valid state */ "Needs Attention" = "Trenger tilsyn"; @@ -722,6 +801,9 @@ /* The title of the Nightscout service */ "Nightscout" = "Nightscout"; +/* Message for mute alert duration selection action sheet */ +"No alerts or alarms will sound while muted. Select how long you would you like to mute for." = "Ingen varsler eller alarmer vil høres mens de er dempet. Velg hvor lenge du ønsker å dempe lyden."; + /* Title for bolus screen notice when no bolus is recommended Title for bolus screen warning when glucose is below suspend threshold, and a bolus is not recommended Title for bolus screen warning when no bolus is recommended */ @@ -777,6 +859,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "OK"; @@ -807,6 +890,9 @@ /* Format string describing retrospective glucose prediction comparison. (1: Predicted glucose)(2: Actual glucose)(3: difference) */ "Predicted: %1$@\nActual: %2$@ (%3$@)" = "Forventet: %1$@\nFaktisk: %2$@ ( %3$@ )"; +/* Format string describing integral retrospective correction. (1: Integral glucose effect)(2: Total glucose effect) */ +"prediction-description-integral-retrospective-correction" = "prediksjon-beskrivelse-integral-retrospektiv-korreksjon"; + /* Preparing critical event log text */ "Preparing Critical Event Logs" = "Forbereder logg av kritiske hendelser"; @@ -890,15 +976,24 @@ /* The title of the notification action to retry a bolus command */ "Retry" = "Prøv på nytt"; +/* No comment provided by engineer. */ +"Save" = "Lagre"; + /* Button text to save carbs and/or manual glucose entry and deliver a bolus */ "Save and Deliver" = "Lagre og gi bolus"; +/* No comment provided by engineer. */ +"Save as favorite food" = "Lagre som favorittmat"; + /* Button text to save carbs and/or manual glucose entry without a bolus */ "Save without Bolusing" = "Lagre uten å sette bolus"; /* Scheduled Delivery status text */ "Scheduled" = "Planlagt"; +/* No comment provided by engineer. */ +"Selecting a favorite food in the carb entry screen automatically fills in the carb quantity, food type, and absorption time fields! Tap the add button below to create your first favorite food!" = "Når du velger en favorittmat i skjermbildet for innlegging av karbohydrater, fylles feltene for karbohydratmengde, matvaretype og opptakstid automatisk ut! Trykk på knappen Legg til nedenfor for å opprette din første favorittmat!"; + /* The title of the services section in settings */ "Services" = "Tjenester"; @@ -942,6 +1037,9 @@ /* The title text in settings */ "Suspend Threshold" = "Terskel for utsettelse"; +/* Title of the prediction input effect for suspension of insulin delivery */ +"Suspension of Insulin Delivery" = "Suspensjon av insulintilførsel"; + /* Descriptive text for button to add CGM device */ "Tap here to set up a CGM" = "Trykk her for å sette opp en CGM"; @@ -960,6 +1058,12 @@ /* Message presented in the status row instructing the user to tap this row to stop a bolus */ "Tap to Stop" = "Trykk for å stoppe"; +/* Label for button to unmute all alerts */ +"Tap to Unmute Alerts" = "Trykk for å dempe varsler"; + +/* The alert body for unmute alert confirmation */ +"Tap Unmute to resume sound for your alerts and alarms." = "Trykk på Slå av lyd for å gjenoppta lyden for varsler og alarmer."; + /* Alert message for a bolus too small validation error */ "The bolus amount entered is smaller than the minimum deliverable." = "Den angitte bolusmengden er mindre enn minimumsleveransen."; @@ -990,6 +1094,9 @@ /* Title text for button to Therapy Settings */ "Therapy Settings" = "Behandlingsinnstillinger"; +/* String shown when glucose based partial application cannot be enabled because dosing strategy is not set to Automatic Bolus */ +"This option only applies when Loop's Dosing Strategy is set to Automatic Bolus." = "Dette alternativet gjelder bare når Loops doseringsstrategi er satt til Automatisk bolus."; + /* Time Sensitive Status text */ "Time Sensitive Notifications" = "Tidssensitive varsler"; @@ -1032,9 +1139,21 @@ /* Unknown amount of time in settings' profile expiration section */ "Unknown time" = "Ukjent tid"; +/* The title of the action used to unmute alerts */ +"Unmute" = "Oppheve demping"; + +/* The alert title for unmute alert confirmation */ +"Unmute Alerts?" = "Oppheve demping av varsler?"; + +/* Error message when a service can't be found to handle a push notification. (1: Service Identifier) */ +"Unsupported Notification Service: %1$@" = "Varslingstjeneste som ikke støttes: %1$@"; + /* The format for the description of a temporary override end date */ "until %@" = "til %@"; +/* indication of when alerts will be unmuted (1: time when alerts unmute) */ +"Until %1$@" = "Inntil %1$@"; + /* The title of a target alert action specifying pre-meal targets duration for 1 hour or until the user enters carbs (whichever comes first). */ "Until I enter carbs" = "Frem til jeg legger inn karbohydrater"; @@ -1068,6 +1187,9 @@ /* No comment provided by engineer. */ "When out of Closed Loop mode, the app uses a simplified bolus calculator like a typical pump." = "Når den er ute av lukket Loop-modus, bruker appen en forenklet boluskalkulator som en vanlig pumpe."; +/* Format string for message of reset loop alert. (1: App name) (2: error description) */ +"While trying to restart %1$@ an error occured.\n\n%2$@" = "Det oppstod en feil da du prøvde å starte %1$@ på nytt.\n\n%2$@"; + /* The label of the workout mode toggle button */ "Workout Targets" = "Målområder for trening"; diff --git a/Loop/nl.lproj/Localizable.strings b/Loop/nl.lproj/Localizable.strings index 29518ca75c..15a1423c3f 100644 --- a/Loop/nl.lproj/Localizable.strings +++ b/Loop/nl.lproj/Localizable.strings @@ -95,6 +95,9 @@ Description of a bolus dose entry (1: title for dose type, 2: value (? if no value) in bold, 3: unit) */ "%1$@: %2$@ %3$@" = "%1$@: %2$@ %3$@"; +/* No comment provided by engineer. */ +"⚠️" = "⚠️"; + /* Description of the prediction input effect for glucose momentum */ "15 min glucose regression coefficient (b₁), continued with decay over 30 min" = "15 min glucose regressiecoëficiënt (b₁), gevolgd door afbouw over 30 min"; @@ -143,6 +146,9 @@ /* The string format describing active insulin. (1: localized insulin value description) */ "Active Insulin: %@" = "Actieve Insuline: %@"; +/* No comment provided by engineer. */ +"Add a new favorite food" = "Voeg een nieuw favoriet eten toe"; + /* Title of the user activity for adding carbs */ "Add Carb Entry" = "Kh. Inv. Toevoegen"; @@ -171,6 +177,13 @@ Notification & Critical Alert Permissions screen title */ "Alert Permissions" = "Toestemming Meldingen"; +/* Navigation title for algorithms experiments screen + The title of the Algorithm Experiments section in settings */ +"Algorithm Experiments" = "Algoritme Experimenten"; + +/* Algorithm Experiments description. */ +"Algorithm Experiments are optional modifications to the Loop Algorithm. These modifications are less tested than the standard Loop Algorithm, so please use carefully." = "Algoritme Experimenten zijn optionele aanpassingen aan het Loop Algoritme. Deze aanpassingen zijn minder grondig getest dan het standaard Loop Algoritme, dus gebruik het voorzichtig."; + /* The title of the section containing algorithm settings */ "Algorithm Settings" = "Algoritme-instellingen"; @@ -303,6 +316,9 @@ /* Description of the prediction input effect for carbohydrates. (1: The glucose unit string) */ "Carbs Absorbed (g) ÷ Carb Ratio (g/U) × Insulin Sensitivity (%1$@/U)" = "Opgenomen Koolhydraten (g) ÷ Koolhydraatratio (g/E) × Insulinegevoeligheid (%1$@/E)"; +/* No comment provided by engineer. */ +"Caution" = "Voorzichtig"; + /* The notification alert describing a low pump battery */ "Change the pump battery immediately" = "Vervang direct de batterij van de pomp"; @@ -507,6 +523,9 @@ /* The alert title for a resume error */ "Failed to Resume Insulin Delivery" = "Insulinetoediening Hervatten Mislukt"; +/* No comment provided by engineer. */ +"FAVORITE FOODS" = "FAVORIETE ETEN"; + /* Title of insulin model preset */ "Fiasp" = "Fiasp"; @@ -707,7 +726,8 @@ /* Details for missing data error when momentum effects are missing */ "Momentum effects" = "Trendlijneffecten"; -/* Text for more info action on notification of upcoming profile expiration */ +/* Text for more info action on notification of upcoming profile expiration + Text for more info action on notification of upcoming TestFlight expiration */ "More Info" = "Meer Informatie"; /* Label for button to mute all alerts */ @@ -777,6 +797,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "Ok"; @@ -893,12 +914,18 @@ /* Button text to save carbs and/or manual glucose entry and deliver a bolus */ "Save and Deliver" = "Opslaan en Toedienen"; +/* No comment provided by engineer. */ +"Save as favorite food" = "Opslaan als favoriet voedsel"; + /* Button text to save carbs and/or manual glucose entry without a bolus */ "Save without Bolusing" = "Opslaan zonder Bolussen"; /* Scheduled Delivery status text */ "Scheduled" = "Gepland"; +/* No comment provided by engineer. */ +"Selecting a favorite food in the carb entry screen automatically fills in the carb quantity, food type, and absorption time fields! Tap the add button below to create your first favorite food!" = "Het selecteren van een favoriet voedsel in het koolhydraat invoerscherm vult automatisch de velden voor de hoeveelheid koolhydraten, het type voedsel en de absorptietijd in! Tik op de toevoegknop hieronder om je eerste favoriete voedsel te maken!"; + /* The title of the services section in settings */ "Services" = "Services"; diff --git a/Loop/pl.lproj/InfoPlist.strings b/Loop/pl.lproj/InfoPlist.strings index f9c24ced75..d581227b06 100644 --- a/Loop/pl.lproj/InfoPlist.strings +++ b/Loop/pl.lproj/InfoPlist.strings @@ -4,6 +4,9 @@ /* Bundle name */ "CFBundleName" = "$(PRODUCT_NAME)"; +/* Privacy - NFC Scan Usage Description */ +"NFCReaderUsageDescription" = "Aplikacja wykorzystuje NFC do parowania z urządzeniami dla diabetyków."; + /* Privacy - Bluetooth Always Usage Description */ "NSBluetoothAlwaysUsageDescription" = "Bluetooth jest używany do komunikacji z pompą i urządzeniami ciągłego monitoringu glukozy."; diff --git a/Loop/pl.lproj/Localizable.strings b/Loop/pl.lproj/Localizable.strings index 7264f52d10..3a365d7036 100644 --- a/Loop/pl.lproj/Localizable.strings +++ b/Loop/pl.lproj/Localizable.strings @@ -95,6 +95,9 @@ Description of a bolus dose entry (1: title for dose type, 2: value (? if no value) in bold, 3: unit) */ "%1$@: %2$@ %3$@" = "%1$@: %2$@ %3$@"; +/* No comment provided by engineer. */ +"⚠️" = "⚠️"; + /* Description of the prediction input effect for glucose momentum */ "15 min glucose regression coefficient (b₁), continued with decay over 30 min" = "15-minutowy współczynnik regresji glukozy (b₁), kontynuowany z rozkładem przez 30 min."; @@ -143,6 +146,9 @@ /* The string format describing active insulin. (1: localized insulin value description) */ "Active Insulin: %@" = "Aktywna Insulina: %@"; +/* No comment provided by engineer. */ +"Add a new favorite food" = "Dodaj nowe ulubione jedzenie"; + /* Title of the user activity for adding carbs */ "Add Carb Entry" = "Wprowadź węglowodany"; @@ -171,9 +177,28 @@ Notification & Critical Alert Permissions screen title */ "Alert Permissions" = "Uprawnienia alertów"; +/* Navigation title for algorithms experiments screen + The title of the Algorithm Experiments section in settings */ +"Algorithm Experiments" = "Algorytmy Eksperymentalne"; + +/* Algorithm Experiments description. */ +"Algorithm Experiments are optional modifications to the Loop Algorithm. These modifications are less tested than the standard Loop Algorithm, so please use carefully." = "Eksperymenty algorytmiczne to opcjonalne modyfikacje algorytmu pętli. Te modyfikacje są mniej przetestowane niż standardowy algorytm pętli, więc używaj ich ostrożnie."; + /* The title of the section containing algorithm settings */ "Algorithm Settings" = "Ustawienia algorytmu"; +/* Warning text for when alerts are muted */ +"All Alerts Muted" = "Wszystkie alerty wyciszone"; + +/* Label for when mute alert will end */ +"All alerts muted until" = "Wszystkie alerty wyciszono do"; + +/* No comment provided by engineer. */ +"All Favorites" = "Wszystkie ulubione"; + +/* Label for carb quantity entry row on carb entry screen */ +"Amount Consumed" = "Ilość węglowodanów"; + /* The title of the Amplitude service */ "Amplitude" = "Amplituda"; @@ -216,6 +241,9 @@ /* Confirmation message for deleting a CGM */ "Are you sure you want to delete this CGM?" = "Czy na pewno chcesz usunąć ten CGM?"; +/* No comment provided by engineer. */ +"Are you sure you want to delete this food?" = "Czy na pewno chcesz usunąć to jedzenie?"; + /* Confirmation message for deleting a service */ "Are you sure you want to delete this service?" = "Czy na pewno chcesz usunąć tę usługę?"; @@ -303,6 +331,9 @@ /* Description of the prediction input effect for carbohydrates. (1: The glucose unit string) */ "Carbs Absorbed (g) ÷ Carb Ratio (g/U) × Insulin Sensitivity (%1$@/U)" = "Ilość węglowodanów (g) ÷ stosunek węglowodanów (g/J) × czułość insuliny (%1$@/J)"; +/* No comment provided by engineer. */ +"Caution" = "Uwaga"; + /* The notification alert describing a low pump battery */ "Change the pump battery immediately" = "Natychmiast wymienić baterię pompy"; @@ -324,6 +355,9 @@ /* Carb entry section footer text explaining absorption time */ "Choose a longer absorption time for larger meals, or those containing fats and proteins. This is only guidance to the algorithm and need not be exact." = "Wybierz dłuższy czas absorpcji dla większych, bogatobiałkowych lub wysokotłuszczowych posiłków. To tylko wskazówka dla algorytmu i nie musi być bardzo dokładna."; +/* No comment provided by engineer. */ +"Choose Favorite:" = "Wybierz ulubione:"; + /* Button title to close view The button label of the action used to dismiss the unsafe notification permission alert */ "Close" = "Zamknij"; @@ -368,6 +402,9 @@ The title text for the glucose target range schedule */ "Correction Range" = "Zakres docelowy"; +/* Format string for title of reset loop alert. (1: App name) */ +"Could Not Restart %1$@" = "Nie można ponownie uruchomić %1$@"; + /* Critical Alerts Status text */ "Critical Alerts" = "Alerty krytyczne"; @@ -401,6 +438,9 @@ /* No comment provided by engineer. */ "Delete" = "Usunąć"; +/* No comment provided by engineer. */ +"Delete “%@”?" = "Usunąć „ %@ ”?"; + /* The title of the button to remove the credentials for a service */ "Delete Account" = "Usuń konto"; @@ -410,6 +450,9 @@ /* Button title to delete CGM */ "Delete CGM" = "Usuń CGM"; +/* No comment provided by engineer. */ +"Delete Food" = "Usuń jedzenie"; + /* Button title to delete a service */ "Delete Service" = "Usuń usługę"; @@ -456,6 +499,12 @@ /* Message to the user to enable bluetooth */ "Enable\nBluetooth" = "Włączać\nBluetooth"; +/* Title for Glucose Based Partial Application toggle */ +"Enable Glucose Based Partial Application" = "Włącz Algorytm adaptacyjny"; + +/* Title for Integral Retrospective Correction toggle */ +"Enable Integral Retrospective Correction" = "Włącz Integralną Korektę Retrospektywną (IRC)"; + /* The action hint of the workout mode toggle button when disabled */ "Enables" = "Włącza"; @@ -507,6 +556,9 @@ /* The alert title for a resume error */ "Failed to Resume Insulin Delivery" = "Nie udało się wznowić podawania insuliny"; +/* No comment provided by engineer. */ +"FAVORITE FOODS" = "ULUBIONE JEDZENIE"; + /* Title of insulin model preset */ "Fiasp" = "Fiasp"; @@ -534,6 +586,10 @@ /* The title of the glucose and prediction graph */ "Glucose" = "Glukoza"; +/* Title for glucose based partial application experiment description + Title of glucose based partial application experiment */ +"Glucose Based Partial Application" = "Algorytm adaptacyjny"; + /* The error message when glucose data is too old to be used. (1: glucose data age in minutes) */ "Glucose data is %1$@ old" = "Dane o glukozie są nieaktualne od %1$@"; @@ -543,6 +599,9 @@ /* Alert title when glucose data returns while on bolus screen */ "Glucose Data Now Available" = "Dane dotyczące glukozy są już dostępne"; +/* Description of the prediction input effect for suspension of insulin delivery */ +"Glucose effect of suspending insulin delivery" = "Wpływ wstrzymania podawania insuliny na poziom glukozy"; + /* Alert title for a manual glucose entry out of range error Title for bolus screen warning when glucose entry is out of range */ "Glucose Entry Out of Range" = "Wprowadzanie glukoza jest poza zakresem"; @@ -559,6 +618,9 @@ /* Immediate Delivery status text */ "Immediate" = "Natychmiastowy"; +/* Algorithm Experiments description second paragraph. */ +"In future versions of Loop these experiments may change, end up as standard parts of the Loop Algorithm, or be removed from Loop entirely. Please follow along in the Loop Zulip chat to stay informed of possible changes to these features." = "W przyszłych wersjach Loop te eksperymenty mogą się zmienić, stać się standardowymi częściami algorytmu Loop lub zostać całkowicie usunięte z Loop. Śledź czat Loop Zulip, aby być na bieżąco z możliwymi zmianami w tych funkcjach."; + /* The title of a target alert action specifying an indefinitely long workout targets duration */ "Indefinitely" = "Niemożliwy do określenia"; @@ -597,9 +659,19 @@ /* Insulin type label */ "Insulin Type" = "Rodzaj insuliny"; +/* Title for integral retrospective correction experiment description + Title of integral retrospective correction experiment */ +"Integral Retrospective Correction" = "Integralna korekta retrospektywna"; + +/* Description of Integral Retrospective Correction toggle. */ +"Integral Retrospective Correction (IRC) is an extension of the standard Retrospective Correction (RC) algorithm component in Loop, which adjusts the forecast based on the history of discrepancies between predicted and actual glucose levels.\n\nIn contrast to RC, which looks at discrepancies over the last 30 minutes, with IRC, the history of discrepancies adds up over time. So continued positive discrepancies over time will result in increased dosing. If the discrepancies are negative over time, Loop will reduce dosing further." = "Integralna korekta retrospektywna (IRC) jest rozszerzeniem standardowego komponentu algorytmu Korekta retrospektywna (RC) w Loop, który koryguje prognozę na podstawie historii rozbieżności między przewidywanymi a rzeczywistymi poziomami glukozy. \n\n W przeciwieństwie do RC, który analizuje rozbieżności w ciągu ostatnich 30 minut, w przypadku IRC historia rozbieżności sumuje się w czasie. Tak więc utrzymujące się dodatnie rozbieżności w czasie spowodują zwiększenie dawki. Jeśli rozbieżności są ujemne w czasie, Loop jeszcze bardziej zmniejszy podawanie insuliny."; + /* Description of an interrupted bolus dose entry (1: title for dose type, 2: value (? if no value) in bold, 3: programmed value (? if no value), 4: unit) */ "Interrupted %1$@: %2$@ of %3$@ %4$@" = "Przerwane %1$@ : %2$@ z %3$@ %4$@"; +/* Carb error description: invalid absorption time. (1: Input duration in hours). */ +"Invalid absorption time: %1$@ hours" = "Nieprawidłowy czas absorpcji: %1$@ godz"; + /* Bolus error description: invalid bolus amount. */ "Invalid Bolus Amount" = "Nieprawidłowa wielkość bolusa"; @@ -658,6 +730,9 @@ /* The notification alert describing a long-lasting loop failure. The substitution parameter is the time interval since the last loop */ "Loop has not completed successfully in %@" = "Loop nie działał poprawnie przez %@"; +/* Description of Glucose Based Partial Application toggle. */ +"Loop normally gives 40% of your predicted insulin needs each dosing cycle.\n\nWhen the Glucose Based Partial Application experiment is enabled, Loop will vary the percentage of recommended bolus delivered each cycle with glucose level.\n\nNear correction range, it will use 20% (similar to Temp Basal), and gradually increase to a maximum of 80% at high glucose (200 mg/dL, 11.1 mmol/L).\n\nPlease be aware that during fast rising glucose, such as after an unannounced meal, this feature, combined with velocity and retrospective correction effects, may result in a larger dose than your ISF would call for." = "Pętla zwykle daje 40%1$ przewidywanego zapotrzebowania na insulinę w każdym cyklu dawkowania. \n\nPo włączeniu eksperymentu częściowego podania insuliny w oparciu o glukozę, Loop będzie zmieniać procent zalecanego bolusa podawanego w każdym cyklu w zależności od poziomu glukozy. \n\nW pobliżu zakresu korekcji będzie zużywać 20%2$ (podobnie jak Baza Tymczasowa) i stopniowo zwiększać do maksimum 80%3$ przy wysokim stężeniu glukozy (200 mg/dl, 11,1 mmol/l). \n\nNależy pamiętać, że podczas szybkiego wzrostu stężenia glukozy, na przykład po niezapowiedzianym posiłku, ta funkcja w połączeniu z szybkością i retrospektywnymi efektami korekcyjnymi może skutkować większą dawką, niż wymagałby ISF."; + /* Description string for automatic bolus dosing strategy */ "Loop will automatically bolus when insulin needs are above scheduled basal, and will use temporary basal rates when needed to reduce insulin delivery below scheduled basal." = "Pętla automatycznie poda bolusa, kiedy zapotrzebowanie na insulinę przekroczy zaplanowaną dawkę podstawową, a w razie potrzeby zredukuje zaplanowaną dawkę podstawową (bazę)."; @@ -707,12 +782,16 @@ /* Details for missing data error when momentum effects are missing */ "Momentum effects" = "wpływ pędu"; -/* Text for more info action on notification of upcoming profile expiration */ +/* Text for more info action on notification of upcoming profile expiration + Text for more info action on notification of upcoming TestFlight expiration */ "More Info" = "Więcej informacji"; /* Label for button to mute all alerts */ "Mute All Alerts" = "Wycisz wszystkie alerty"; +/* Title for mute alert duration selection action sheet */ +"Mute All Alerts Temporarily" = "Tymczasowo wycisz wszystkie alerty"; + /* Sensor state description for the non-valid state */ "Needs Attention" = "Potrzebuje uwagi"; @@ -722,6 +801,9 @@ /* The title of the Nightscout service */ "Nightscout" = "Nightscout"; +/* Message for mute alert duration selection action sheet */ +"No alerts or alarms will sound while muted. Select how long you would you like to mute for." = "Po wyciszeniu nie będą emitowane żadne alerty ani alarmy. Wybierz, jak długo chcesz wyciszyć."; + /* Title for bolus screen notice when no bolus is recommended Title for bolus screen warning when glucose is below suspend threshold, and a bolus is not recommended Title for bolus screen warning when no bolus is recommended */ @@ -777,6 +859,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "OK"; @@ -807,6 +890,9 @@ /* Format string describing retrospective glucose prediction comparison. (1: Predicted glucose)(2: Actual glucose)(3: difference) */ "Predicted: %1$@\nActual: %2$@ (%3$@)" = "Przewidywana: %1$@Rzeczywista: %2$@ (%3$@)"; +/* Format string describing integral retrospective correction. (1: Integral glucose effect)(2: Total glucose effect) */ +"prediction-description-integral-retrospective-correction" = "predykcja-opis-całka-retrospektywna-korekta"; + /* Preparing critical event log text */ "Preparing Critical Event Logs" = "Przygotowywanie dzienników zdarzeń krytycznych"; @@ -890,15 +976,24 @@ /* The title of the notification action to retry a bolus command */ "Retry" = "Spróbuj ponownie"; +/* No comment provided by engineer. */ +"Save" = "Zapisz"; + /* Button text to save carbs and/or manual glucose entry and deliver a bolus */ "Save and Deliver" = "Zapisz i podaj"; +/* No comment provided by engineer. */ +"Save as favorite food" = "Zapisz jako ulubione jedzenie"; + /* Button text to save carbs and/or manual glucose entry without a bolus */ "Save without Bolusing" = "Zapisz bez podania Bolusa"; /* Scheduled Delivery status text */ "Scheduled" = "Zaplanowane"; +/* No comment provided by engineer. */ +"Selecting a favorite food in the carb entry screen automatically fills in the carb quantity, food type, and absorption time fields! Tap the add button below to create your first favorite food!" = "Wybór ulubionego jedzenia na ekranie wprowadzania węglowodanów powoduje automatyczne wypełnienie pól ilości węglowodanów, rodzaju jedzenia i czasu wchłaniania! Dotknij przycisku dodawania poniżej, aby stworzyć swoje pierwsze ulubione jedzenie!"; + /* The title of the services section in settings */ "Services" = "Usługi"; @@ -942,6 +1037,9 @@ /* The title text in settings */ "Suspend Threshold" = "Próg zawieszenia pompy"; +/* Title of the prediction input effect for suspension of insulin delivery */ +"Suspension of Insulin Delivery" = "Wstrzymanie podawania insuliny"; + /* Descriptive text for button to add CGM device */ "Tap here to set up a CGM" = "Stuknij tutaj, aby skonfigurować CGM"; @@ -960,6 +1058,12 @@ /* Message presented in the status row instructing the user to tap this row to stop a bolus */ "Tap to Stop" = "Bolus STOP!"; +/* Label for button to unmute all alerts */ +"Tap to Unmute Alerts" = "Stuknij, aby wyłączyć wyciszenie alertów"; + +/* The alert body for unmute alert confirmation */ +"Tap Unmute to resume sound for your alerts and alarms." = "Stuknij opcję Wyłącz wyciszenie, aby wznowić dźwięk alertów i alarmów."; + /* Alert message for a bolus too small validation error */ "The bolus amount entered is smaller than the minimum deliverable." = "Wprowadzona wielkość bolusa jest mniejsza niż minimalna możliwa do podania."; @@ -990,6 +1094,9 @@ /* Title text for button to Therapy Settings */ "Therapy Settings" = "Ustawienia terapii"; +/* String shown when glucose based partial application cannot be enabled because dosing strategy is not set to Automatic Bolus */ +"This option only applies when Loop's Dosing Strategy is set to Automatic Bolus." = "Ta opcja ma zastosowanie tylko wtedy, gdy Strategia dawkowania pętli jest ustawiona na Automatyczny bolus."; + /* Time Sensitive Status text */ "Time Sensitive Notifications" = "Powiadomienia zależne od czasu"; @@ -1032,9 +1139,21 @@ /* Unknown amount of time in settings' profile expiration section */ "Unknown time" = "Nieznany czas"; +/* The title of the action used to unmute alerts */ +"Unmute" = "Wyłącz wyciszenie"; + +/* The alert title for unmute alert confirmation */ +"Unmute Alerts?" = "Wyciszyć Alerty?"; + +/* Error message when a service can't be found to handle a push notification. (1: Service Identifier) */ +"Unsupported Notification Service: %1$@" = "Nieobsługiwana usługa powiadomień: %1$@"; + /* The format for the description of a temporary override end date */ "until %@" = "do %@"; +/* indication of when alerts will be unmuted (1: time when alerts unmute) */ +"Until %1$@" = "Do %1$@"; + /* The title of a target alert action specifying pre-meal targets duration for 1 hour or until the user enters carbs (whichever comes first). */ "Until I enter carbs" = "Dopóki nie wprowadzę węglowodanów"; @@ -1068,6 +1187,9 @@ /* No comment provided by engineer. */ "When out of Closed Loop mode, the app uses a simplified bolus calculator like a typical pump." = "Poza trybem pętli zamkniętej aplikacja korzysta z uproszczonego kalkulatora bolusa, takiego jak typowa pompa."; +/* Format string for message of reset loop alert. (1: App name) (2: error description) */ +"While trying to restart %1$@ an error occured.\n\n%2$@" = "Podczas próby ponownego uruchomienia %1$@ wystąpił błąd. \n\n %2$@"; + /* The label of the workout mode toggle button */ "Workout Targets" = "Zakres w czasie wysiłku fizycznego"; diff --git a/Loop/pt-BR.lproj/Localizable.strings b/Loop/pt-BR.lproj/Localizable.strings index d3d565ca81..cb0345df80 100644 --- a/Loop/pt-BR.lproj/Localizable.strings +++ b/Loop/pt-BR.lproj/Localizable.strings @@ -331,7 +331,8 @@ /* Details for missing data error when momentum effects are missing */ "Momentum effects" = "Efeitos de aceleração"; -/* Text for more info action on notification of upcoming profile expiration */ +/* Text for more info action on notification of upcoming profile expiration + Text for more info action on notification of upcoming TestFlight expiration */ "More Info" = "Mais Info"; /* Sensor state description for the non-valid state */ @@ -348,6 +349,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "OK"; diff --git a/Loop/ro.lproj/Localizable.strings b/Loop/ro.lproj/Localizable.strings index bec9c5c23d..3a0da3f688 100644 --- a/Loop/ro.lproj/Localizable.strings +++ b/Loop/ro.lproj/Localizable.strings @@ -707,7 +707,8 @@ /* Details for missing data error when momentum effects are missing */ "Momentum effects" = "Efecte momentum"; -/* Text for more info action on notification of upcoming profile expiration */ +/* Text for more info action on notification of upcoming profile expiration + Text for more info action on notification of upcoming TestFlight expiration */ "More Info" = "Detalii"; /* Label for button to mute all alerts */ @@ -777,6 +778,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "OK"; diff --git a/Loop/ru.lproj/Localizable.strings b/Loop/ru.lproj/Localizable.strings index ded27d134b..43cb4146cb 100644 --- a/Loop/ru.lproj/Localizable.strings +++ b/Loop/ru.lproj/Localizable.strings @@ -95,6 +95,9 @@ Description of a bolus dose entry (1: title for dose type, 2: value (? if no value) in bold, 3: unit) */ "%1$@: %2$@ %3$@" = "%1$@: %2$@ %3$@"; +/* No comment provided by engineer. */ +"⚠️" = "⚠️"; + /* Description of the prediction input effect for glucose momentum */ "15 min glucose regression coefficient (b₁), continued with decay over 30 min" = "15-мин коэффициент регрессии гликемии (b1), продолжен с угасанием 30 мин"; @@ -707,7 +710,8 @@ /* Details for missing data error when momentum effects are missing */ "Momentum effects" = "Влияние динамики СК"; -/* Text for more info action on notification of upcoming profile expiration */ +/* Text for more info action on notification of upcoming profile expiration + Text for more info action on notification of upcoming TestFlight expiration */ "More Info" = "Доп. инфо"; /* Label for button to mute all alerts */ @@ -777,6 +781,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "OK"; @@ -807,6 +812,9 @@ /* Format string describing retrospective glucose prediction comparison. (1: Predicted glucose)(2: Actual glucose)(3: difference) */ "Predicted: %1$@\nActual: %2$@ (%3$@)" = "Прогноз: %1$@\nФакт: %2$@ (%3$@)"; +/* Format string describing integral retrospective correction. (1: Integral glucose effect)(2: Total glucose effect) */ +"prediction-description-integral-retrospective-correction" = "описание прогнозирования с помощью интегральной ретроспективной коррекции"; + /* Preparing critical event log text */ "Preparing Critical Event Logs" = "Подготовка логов критических событий"; @@ -990,6 +998,9 @@ /* Title text for button to Therapy Settings */ "Therapy Settings" = "Настройки терапии"; +/* String shown when glucose based partial application cannot be enabled because dosing strategy is not set to Automatic Bolus */ +"This option only applies when Loop's Dosing Strategy is set to Automatic Bolus." = "Эта опция применима только в том случае, если для стратегии дозирования петли установлено значение «Автоматический болюс»."; + /* Time Sensitive Status text */ "Time Sensitive Notifications" = "Уведомления, чувствительные к времени"; @@ -1032,9 +1043,21 @@ /* Unknown amount of time in settings' profile expiration section */ "Unknown time" = "Неизвестное время"; +/* The title of the action used to unmute alerts */ +"Unmute" = "Включить звук"; + +/* The alert title for unmute alert confirmation */ +"Unmute Alerts?" = "Включить звук оповещений?"; + +/* Error message when a service can't be found to handle a push notification. (1: Service Identifier) */ +"Unsupported Notification Service: %1$@" = "Неподдерживаемая служба уведомлений: %1$@"; + /* The format for the description of a temporary override end date */ "until %@" = "до %@"; +/* indication of when alerts will be unmuted (1: time when alerts unmute) */ +"Until %1$@" = "До %1$@"; + /* The title of a target alert action specifying pre-meal targets duration for 1 hour or until the user enters carbs (whichever comes first). */ "Until I enter carbs" = "Пока я не введу углеводы"; @@ -1068,6 +1091,9 @@ /* No comment provided by engineer. */ "When out of Closed Loop mode, the app uses a simplified bolus calculator like a typical pump." = "Когда приложение выходит из режима замкнутого цикла, оно использует упрощенный калькулятор болюса, как в обычной помпе."; +/* Format string for message of reset loop alert. (1: App name) (2: error description) */ +"While trying to restart %1$@ an error occured.\n\n%2$@" = "При попытке перезапустить %1$@ произошла ошибка. \n\n %2$@"; + /* The label of the workout mode toggle button */ "Workout Targets" = "Целевые значения при физической нагрузке"; diff --git a/Loop/sk.lproj/Localizable.strings b/Loop/sk.lproj/Localizable.strings index e0820a4f00..a1bbaec38d 100644 --- a/Loop/sk.lproj/Localizable.strings +++ b/Loop/sk.lproj/Localizable.strings @@ -231,6 +231,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "OK"; diff --git a/Loop/sv.lproj/Localizable.strings b/Loop/sv.lproj/Localizable.strings index 838ee70ed2..ceec268207 100644 --- a/Loop/sv.lproj/Localizable.strings +++ b/Loop/sv.lproj/Localizable.strings @@ -543,7 +543,8 @@ /* Details for missing data error when momentum effects are missing */ "Momentum effects" = "Momentumeffekter"; -/* Text for more info action on notification of upcoming profile expiration */ +/* Text for more info action on notification of upcoming profile expiration + Text for more info action on notification of upcoming TestFlight expiration */ "More Info" = "Mer info"; /* Sensor state description for the non-valid state */ @@ -583,6 +584,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "OK"; diff --git a/Loop/tr.lproj/Localizable.strings b/Loop/tr.lproj/Localizable.strings index a3da653f5c..43663a02b4 100644 --- a/Loop/tr.lproj/Localizable.strings +++ b/Loop/tr.lproj/Localizable.strings @@ -707,7 +707,8 @@ /* Details for missing data error when momentum effects are missing */ "Momentum effects" = "Momentum etkileri"; -/* Text for more info action on notification of upcoming profile expiration */ +/* Text for more info action on notification of upcoming profile expiration + Text for more info action on notification of upcoming TestFlight expiration */ "More Info" = "Daha fazla bilgi"; /* Label for button to mute all alerts */ @@ -777,6 +778,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "Tamam"; diff --git a/Loop/vi.lproj/Localizable.strings b/Loop/vi.lproj/Localizable.strings index b3935045e6..1ee202e94e 100644 --- a/Loop/vi.lproj/Localizable.strings +++ b/Loop/vi.lproj/Localizable.strings @@ -331,7 +331,8 @@ /* Details for missing data error when momentum effects are missing */ "Momentum effects" = "Hiệu ứng động lượng"; -/* Text for more info action on notification of upcoming profile expiration */ +/* Text for more info action on notification of upcoming profile expiration + Text for more info action on notification of upcoming TestFlight expiration */ "More Info" = "Thêm thông tin"; /* Sensor state description for the non-valid state */ @@ -348,6 +349,7 @@ Default action for alert when alert acknowledgment fails Notifications permissions disabled alert button Text for ok action on notification of upcoming profile expiration + Text for ok action on notification of upcoming TestFlight expiration The title of the notification action to acknowledge a device alert */ "OK" = "OK"; diff --git a/LoopTests/Fixtures/live_capture/live_capture_carb_entries.json b/LoopTests/Fixtures/live_capture/live_capture_carb_entries.json deleted file mode 100644 index 6c5b77202e..0000000000 --- a/LoopTests/Fixtures/live_capture/live_capture_carb_entries.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "startDate": "2023-07-29T18:17:07Z", - "quantity": 50 - } -] diff --git a/LoopTests/Fixtures/live_capture/live_capture_doses.json b/LoopTests/Fixtures/live_capture/live_capture_doses.json deleted file mode 100644 index 452fce1d0a..0000000000 --- a/LoopTests/Fixtures/live_capture/live_capture_doses.json +++ /dev/null @@ -1,793 +0,0 @@ -[ - { - "value": 0, - "startDate": "2023-07-28T19:15:55Z", - "type": "tempBasal", - "endDate": "2023-07-28T19:15:55Z", - "unit": "U/hour" - }, - { - "value": 0.20000000000000001, - "startDate": "2023-07-28T19:35:49Z", - "type": "tempBasal", - "endDate": "2023-07-28T19:35:49Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-28T19:40:49Z", - "type": "tempBasal", - "endDate": "2023-07-28T19:40:49Z", - "unit": "U/hour" - }, - { - "value": 0.25, - "startDate": "2023-07-28T20:00:49Z", - "type": "bolus", - "endDate": "2023-07-28T20:00:49Z", - "unit": "U" - }, - { - "value": 0.25, - "startDate": "2023-07-28T20:05:48Z", - "type": "bolus", - "endDate": "2023-07-28T20:05:48Z", - "unit": "U" - }, - { - "value": 0.40000000000000002, - "startDate": "2023-07-28T20:10:51Z", - "type": "bolus", - "endDate": "2023-07-28T20:10:51Z", - "unit": "U" - }, - { - "value": 0.34999999999999998, - "startDate": "2023-07-28T20:15:49Z", - "type": "bolus", - "endDate": "2023-07-28T20:15:49Z", - "unit": "U" - }, - { - "value": 0.5, - "startDate": "2023-07-28T20:20:52Z", - "type": "bolus", - "endDate": "2023-07-28T20:20:52Z", - "unit": "U" - }, - { - "value": 0.40000000000000002, - "startDate": "2023-07-28T20:30:48Z", - "type": "bolus", - "endDate": "2023-07-28T20:30:48Z", - "unit": "U" - }, - { - "value": 0.10000000000000001, - "startDate": "2023-07-28T20:35:51Z", - "type": "bolus", - "endDate": "2023-07-28T20:35:51Z", - "unit": "U" - }, - { - "value": 0.25, - "startDate": "2023-07-28T20:40:51Z", - "type": "bolus", - "endDate": "2023-07-28T20:40:51Z", - "unit": "U" - }, - { - "value": 0.80000000000000004, - "startDate": "2023-07-28T20:45:56Z", - "type": "tempBasal", - "endDate": "2023-07-28T20:45:56Z", - "unit": "U/hour" - }, - { - "value": 0.40000000000000002, - "startDate": "2023-07-28T20:55:48Z", - "type": "tempBasal", - "endDate": "2023-07-28T20:55:48Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-28T20:59:35Z", - "type": "suspend", - "endDate": "2023-07-28T20:59:35Z", - "unit": "U" - }, - { - "value": 3.0499999999999998, - "startDate": "2023-07-29T00:07:52Z", - "type": "bolus", - "endDate": "2023-07-29T00:07:52Z", - "unit": "U" - }, - { - "value": 0.40000000000000002, - "startDate": "2023-07-29T00:10:54Z", - "type": "bolus", - "endDate": "2023-07-29T00:10:54Z", - "unit": "U" - }, - { - "value": 0.5, - "startDate": "2023-07-29T00:15:49Z", - "type": "bolus", - "endDate": "2023-07-29T00:15:49Z", - "unit": "U" - }, - { - "value": 0.40000000000000002, - "startDate": "2023-07-29T00:20:50Z", - "type": "bolus", - "endDate": "2023-07-29T00:20:50Z", - "unit": "U" - }, - { - "value": 0.40000000000000002, - "startDate": "2023-07-29T00:25:49Z", - "type": "bolus", - "endDate": "2023-07-29T00:25:49Z", - "unit": "U" - }, - { - "value": 0.55000000000000004, - "startDate": "2023-07-29T00:30:52Z", - "type": "bolus", - "endDate": "2023-07-29T00:30:52Z", - "unit": "U" - }, - { - "value": 0.59999999999999998, - "startDate": "2023-07-29T00:35:50Z", - "type": "bolus", - "endDate": "2023-07-29T00:35:50Z", - "unit": "U" - }, - { - "value": 0.40000000000000002, - "startDate": "2023-07-29T00:40:50Z", - "type": "bolus", - "endDate": "2023-07-29T00:40:50Z", - "unit": "U" - }, - { - "value": 0.14999999999999999, - "startDate": "2023-07-29T00:45:49Z", - "type": "bolus", - "endDate": "2023-07-29T00:45:49Z", - "unit": "U" - }, - { - "value": 0.10000000000000001, - "startDate": "2023-07-29T00:55:50Z", - "type": "tempBasal", - "endDate": "2023-07-29T00:55:50Z", - "unit": "U/hour" - }, - { - "value": 0.29999999999999999, - "startDate": "2023-07-29T01:00:49Z", - "type": "tempBasal", - "endDate": "2023-07-29T01:00:49Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T01:05:49Z", - "type": "tempBasal", - "endDate": "2023-07-29T01:05:49Z", - "unit": "U/hour" - }, - { - "value": 0.14999999999999999, - "startDate": "2023-07-29T01:15:49Z", - "type": "tempBasal", - "endDate": "2023-07-29T01:15:49Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T01:20:50Z", - "type": "tempBasal", - "endDate": "2023-07-29T01:20:50Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T01:45:49Z", - "type": "tempBasal", - "endDate": "2023-07-29T01:45:49Z", - "unit": "U/hour" - }, - { - "value": 0.050000000000000003, - "startDate": "2023-07-29T01:50:50Z", - "type": "bolus", - "endDate": "2023-07-29T01:50:50Z", - "unit": "U" - }, - { - "value": 0.34999999999999998, - "startDate": "2023-07-29T02:00:58Z", - "type": "tempBasal", - "endDate": "2023-07-29T02:00:58Z", - "unit": "U/hour" - }, - { - "value": 0.050000000000000003, - "startDate": "2023-07-29T02:00:59Z", - "type": "tempBasal", - "endDate": "2023-07-29T02:00:59Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T02:06:04Z", - "type": "tempBasal", - "endDate": "2023-07-29T02:06:04Z", - "unit": "U/hour" - }, - { - "value": 0.20000000000000001, - "startDate": "2023-07-29T02:10:55Z", - "type": "bolus", - "endDate": "2023-07-29T02:10:55Z", - "unit": "U" - }, - { - "value": 0.10000000000000001, - "startDate": "2023-07-29T02:15:49Z", - "type": "bolus", - "endDate": "2023-07-29T02:15:49Z", - "unit": "U" - }, - { - "value": 0.10000000000000001, - "startDate": "2023-07-29T02:21:02Z", - "type": "bolus", - "endDate": "2023-07-29T02:21:02Z", - "unit": "U" - }, - { - "value": 0.20000000000000001, - "startDate": "2023-07-29T02:30:49Z", - "type": "bolus", - "endDate": "2023-07-29T02:30:49Z", - "unit": "U" - }, - { - "value": 0.25, - "startDate": "2023-07-29T02:35:55Z", - "type": "bolus", - "endDate": "2023-07-29T02:35:55Z", - "unit": "U" - }, - { - "value": 0.5, - "startDate": "2023-07-29T02:45:58Z", - "type": "bolus", - "endDate": "2023-07-29T02:45:58Z", - "unit": "U" - }, - { - "value": 0.29999999999999999, - "startDate": "2023-07-29T02:50:54Z", - "type": "bolus", - "endDate": "2023-07-29T02:50:54Z", - "unit": "U" - }, - { - "value": 0.25, - "startDate": "2023-07-29T02:55:59Z", - "type": "bolus", - "endDate": "2023-07-29T02:55:59Z", - "unit": "U" - }, - { - "value": 0.14999999999999999, - "startDate": "2023-07-29T03:00:49Z", - "type": "bolus", - "endDate": "2023-07-29T03:00:49Z", - "unit": "U" - }, - { - "value": 0.050000000000000003, - "startDate": "2023-07-29T03:05:51Z", - "type": "bolus", - "endDate": "2023-07-29T03:05:51Z", - "unit": "U" - }, - { - "value": 0.14999999999999999, - "startDate": "2023-07-29T03:10:58Z", - "type": "bolus", - "endDate": "2023-07-29T03:10:58Z", - "unit": "U" - }, - { - "value": 0.050000000000000003, - "startDate": "2023-07-29T03:20:59Z", - "type": "bolus", - "endDate": "2023-07-29T03:20:59Z", - "unit": "U" - }, - { - "value": 0.20000000000000001, - "startDate": "2023-07-29T03:30:50Z", - "type": "bolus", - "endDate": "2023-07-29T03:30:50Z", - "unit": "U" - }, - { - "value": 0.14999999999999999, - "startDate": "2023-07-29T03:35:51Z", - "type": "bolus", - "endDate": "2023-07-29T03:35:51Z", - "unit": "U" - }, - { - "value": 0.25, - "startDate": "2023-07-29T03:45:51Z", - "type": "tempBasal", - "endDate": "2023-07-29T03:45:51Z", - "unit": "U/hour" - }, - { - "value": 0.45000000000000001, - "startDate": "2023-07-29T03:50:50Z", - "type": "tempBasal", - "endDate": "2023-07-29T03:50:50Z", - "unit": "U/hour" - }, - { - "value": 0.10000000000000001, - "startDate": "2023-07-29T03:55:49Z", - "type": "tempBasal", - "endDate": "2023-07-29T03:55:49Z", - "unit": "U/hour" - }, - { - "value": 0.29999999999999999, - "startDate": "2023-07-29T04:00:50Z", - "type": "tempBasal", - "endDate": "2023-07-29T04:00:50Z", - "unit": "U/hour" - }, - { - "value": 0.14999999999999999, - "startDate": "2023-07-29T04:05:51Z", - "type": "tempBasal", - "endDate": "2023-07-29T04:05:51Z", - "unit": "U/hour" - }, - { - "value": 0.5, - "startDate": "2023-07-29T04:10:49Z", - "type": "tempBasal", - "endDate": "2023-07-29T04:10:49Z", - "unit": "U/hour" - }, - { - "value": 0.34999999999999998, - "startDate": "2023-07-29T04:25:49Z", - "type": "tempBasal", - "endDate": "2023-07-29T04:25:49Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T04:36:12Z", - "type": "tempBasal", - "endDate": "2023-07-29T04:36:12Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T05:00:49Z", - "type": "tempBasal", - "endDate": "2023-07-29T05:00:49Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T05:20:53Z", - "type": "tempBasal", - "endDate": "2023-07-29T05:20:53Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T05:45:52Z", - "type": "tempBasal", - "endDate": "2023-07-29T05:45:52Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T06:10:49Z", - "type": "tempBasal", - "endDate": "2023-07-29T06:10:49Z", - "unit": "U/hour" - }, - { - "value": 1.05, - "startDate": "2023-07-29T06:40:50Z", - "type": "bolus", - "endDate": "2023-07-29T06:40:50Z", - "unit": "U" - }, - { - "value": 0.69999999999999996, - "startDate": "2023-07-29T06:45:49Z", - "type": "bolus", - "endDate": "2023-07-29T06:45:49Z", - "unit": "U" - }, - { - "value": 0.40000000000000002, - "startDate": "2023-07-29T06:50:50Z", - "type": "bolus", - "endDate": "2023-07-29T06:50:50Z", - "unit": "U" - }, - { - "value": 0.59999999999999998, - "startDate": "2023-07-29T06:55:52Z", - "type": "bolus", - "endDate": "2023-07-29T06:55:52Z", - "unit": "U" - }, - { - "value": 0.5, - "startDate": "2023-07-29T07:00:49Z", - "type": "bolus", - "endDate": "2023-07-29T07:00:49Z", - "unit": "U" - }, - { - "value": 0.80000000000000004, - "startDate": "2023-07-29T07:05:52Z", - "type": "bolus", - "endDate": "2023-07-29T07:05:52Z", - "unit": "U" - }, - { - "value": 0.69999999999999996, - "startDate": "2023-07-29T07:10:51Z", - "type": "bolus", - "endDate": "2023-07-29T07:10:51Z", - "unit": "U" - }, - { - "value": 0.10000000000000001, - "startDate": "2023-07-29T07:15:50Z", - "type": "bolus", - "endDate": "2023-07-29T07:15:50Z", - "unit": "U" - }, - { - "value": 0, - "startDate": "2023-07-29T07:20:50Z", - "type": "tempBasal", - "endDate": "2023-07-29T07:20:50Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T07:45:58Z", - "type": "tempBasal", - "endDate": "2023-07-29T07:45:58Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T08:10:53Z", - "type": "tempBasal", - "endDate": "2023-07-29T08:10:53Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T08:30:55Z", - "type": "tempBasal", - "endDate": "2023-07-29T08:30:55Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T08:55:56Z", - "type": "tempBasal", - "endDate": "2023-07-29T08:55:56Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T09:20:52Z", - "type": "tempBasal", - "endDate": "2023-07-29T09:20:52Z", - "unit": "U/hour" - }, - { - "value": 0.10000000000000001, - "startDate": "2023-07-29T10:00:51Z", - "type": "bolus", - "endDate": "2023-07-29T10:00:51Z", - "unit": "U" - }, - { - "value": 0.050000000000000003, - "startDate": "2023-07-29T10:05:58Z", - "type": "bolus", - "endDate": "2023-07-29T10:05:58Z", - "unit": "U" - }, - { - "value": 0.050000000000000003, - "startDate": "2023-07-29T10:20:52Z", - "type": "bolus", - "endDate": "2023-07-29T10:20:52Z", - "unit": "U" - }, - { - "value": 0.050000000000000003, - "startDate": "2023-07-29T10:25:50Z", - "type": "bolus", - "endDate": "2023-07-29T10:25:50Z", - "unit": "U" - }, - { - "value": 0.050000000000000003, - "startDate": "2023-07-29T10:30:53Z", - "type": "bolus", - "endDate": "2023-07-29T10:30:53Z", - "unit": "U" - }, - { - "value": 0, - "startDate": "2023-07-29T11:25:51Z", - "type": "tempBasal", - "endDate": "2023-07-29T11:25:51Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T12:50:53Z", - "type": "tempBasal", - "endDate": "2023-07-29T12:50:53Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T13:00:54Z", - "type": "tempBasal", - "endDate": "2023-07-29T13:00:54Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T13:25:52Z", - "type": "tempBasal", - "endDate": "2023-07-29T13:25:52Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T13:50:53Z", - "type": "tempBasal", - "endDate": "2023-07-29T13:50:53Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T14:15:53Z", - "type": "tempBasal", - "endDate": "2023-07-29T14:15:53Z", - "unit": "U/hour" - }, - { - "value": 0.65000000000000002, - "startDate": "2023-07-29T15:00:51Z", - "type": "bolus", - "endDate": "2023-07-29T15:00:51Z", - "unit": "U" - }, - { - "value": 0.65000000000000002, - "startDate": "2023-07-29T15:05:51Z", - "type": "bolus", - "endDate": "2023-07-29T15:05:51Z", - "unit": "U" - }, - { - "value": 0.20000000000000001, - "startDate": "2023-07-29T15:10:53Z", - "type": "bolus", - "endDate": "2023-07-29T15:10:53Z", - "unit": "U" - }, - { - "value": 0.40000000000000002, - "startDate": "2023-07-29T15:15:51Z", - "type": "bolus", - "endDate": "2023-07-29T15:15:51Z", - "unit": "U" - }, - { - "value": 0.29999999999999999, - "startDate": "2023-07-29T15:21:14Z", - "type": "bolus", - "endDate": "2023-07-29T15:21:14Z", - "unit": "U" - }, - { - "value": 0.69999999999999996, - "startDate": "2023-07-29T15:30:51Z", - "type": "tempBasal", - "endDate": "2023-07-29T15:30:51Z", - "unit": "U/hour" - }, - { - "value": 0.40000000000000002, - "startDate": "2023-07-29T15:35:53Z", - "type": "bolus", - "endDate": "2023-07-29T15:35:53Z", - "unit": "U" - }, - { - "value": 0.59999999999999998, - "startDate": "2023-07-29T15:40:51Z", - "type": "bolus", - "endDate": "2023-07-29T15:40:51Z", - "unit": "U" - }, - { - "value": 0.10000000000000001, - "startDate": "2023-07-29T15:45:52Z", - "type": "bolus", - "endDate": "2023-07-29T15:45:52Z", - "unit": "U" - }, - { - "value": 0.40000000000000002, - "startDate": "2023-07-29T15:50:50Z", - "type": "bolus", - "endDate": "2023-07-29T15:50:50Z", - "unit": "U" - }, - { - "value": 0.59999999999999998, - "startDate": "2023-07-29T15:55:53Z", - "type": "bolus", - "endDate": "2023-07-29T15:55:53Z", - "unit": "U" - }, - { - "value": 0.20000000000000001, - "startDate": "2023-07-29T16:00:51Z", - "type": "bolus", - "endDate": "2023-07-29T16:00:51Z", - "unit": "U" - }, - { - "value": 0.10000000000000001, - "startDate": "2023-07-29T16:05:51Z", - "type": "bolus", - "endDate": "2023-07-29T16:05:51Z", - "unit": "U" - }, - { - "value": 0.25, - "startDate": "2023-07-29T16:15:51Z", - "type": "bolus", - "endDate": "2023-07-29T16:15:51Z", - "unit": "U" - }, - { - "value": 0.20000000000000001, - "startDate": "2023-07-29T16:20:50Z", - "type": "bolus", - "endDate": "2023-07-29T16:20:50Z", - "unit": "U" - }, - { - "value": 0, - "startDate": "2023-07-29T16:25:54Z", - "type": "tempBasal", - "endDate": "2023-07-29T16:25:54Z", - "unit": "U/hour" - }, - { - "value": 0.29999999999999999, - "startDate": "2023-07-29T16:55:51Z", - "type": "tempBasal", - "endDate": "2023-07-29T16:55:51Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T17:00:51Z", - "type": "tempBasal", - "endDate": "2023-07-29T17:00:51Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T17:25:54Z", - "type": "tempBasal", - "endDate": "2023-07-29T17:25:54Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T17:45:56Z", - "type": "tempBasal", - "endDate": "2023-07-29T17:45:56Z", - "unit": "U/hour" - }, - { - "value": 0, - "startDate": "2023-07-29T18:10:56Z", - "type": "tempBasal", - "endDate": "2023-07-29T18:10:56Z", - "unit": "U/hour" - }, - { - "value": 0.20000000000000001, - "startDate": "2023-07-29T18:15:56Z", - "type": "bolus", - "endDate": "2023-07-29T18:15:56Z", - "unit": "U" - }, - { - "value": 4.0999999999999996, - "startDate": "2023-07-29T18:17:11Z", - "type": "bolus", - "endDate": "2023-07-29T18:17:11Z", - "unit": "U" - }, - { - "value": 0.29999999999999999, - "startDate": "2023-07-29T18:25:55Z", - "type": "bolus", - "endDate": "2023-07-29T18:25:55Z", - "unit": "U" - }, - { - "value": 0, - "startDate": "2023-07-29T18:40:51Z", - "type": "tempBasal", - "endDate": "2023-07-29T18:40:51Z", - "unit": "U/hour" - }, - { - "value": 0.34999999999999998, - "startDate": "2023-07-29T18:51:05Z", - "type": "tempBasal", - "endDate": "2023-07-29T18:51:05Z", - "unit": "U/hour" - }, - { - "value": 0.55000000000000004, - "startDate": "2023-07-29T18:51:07Z", - "type": "tempBasal", - "endDate": "2023-07-29T18:51:07Z", - "unit": "U/hour" - }, - { - "value": 0.20000000000000001, - "startDate": "2023-07-29T18:55:55Z", - "type": "tempBasal", - "endDate": "2023-07-29T18:55:55Z", - "unit": "U/hour" - }, - { - "value": 0.5, - "startDate": "2023-07-29T19:00:57Z", - "type": "tempBasal", - "endDate": "2023-07-29T19:00:57Z", - "unit": "U/hour" - } -] diff --git a/LoopTests/Fixtures/live_capture/live_capture_historic_glucose.json b/LoopTests/Fixtures/live_capture/live_capture_historic_glucose.json deleted file mode 100644 index 0da74e48fc..0000000000 --- a/LoopTests/Fixtures/live_capture/live_capture_historic_glucose.json +++ /dev/null @@ -1,854 +0,0 @@ -[ - { - "startDate": "2023-07-29T01:40:51Z", - "quantity": 254 - }, - { - "startDate": "2023-07-29T01:45:52Z", - "quantity": 259 - }, - { - "startDate": "2023-07-29T01:50:51Z", - "quantity": 252 - }, - { - "startDate": "2023-07-29T01:55:52Z", - "quantity": 250 - }, - { - "startDate": "2023-07-29T02:00:52Z", - "quantity": 234 - }, - { - "startDate": "2023-07-29T02:05:51Z", - "quantity": 222 - }, - { - "startDate": "2023-07-29T02:10:51Z", - "quantity": 233 - }, - { - "startDate": "2023-07-29T02:15:52Z", - "quantity": 235 - }, - { - "startDate": "2023-07-29T02:20:52Z", - "quantity": 231 - }, - { - "startDate": "2023-07-29T02:25:52Z", - "quantity": 235 - }, - { - "startDate": "2023-07-29T02:30:51Z", - "quantity": 232 - }, - { - "startDate": "2023-07-29T02:35:51Z", - "quantity": 235 - }, - { - "startDate": "2023-07-29T02:40:52Z", - "quantity": 244 - }, - { - "startDate": "2023-07-29T02:45:51Z", - "quantity": 247 - }, - { - "startDate": "2023-07-29T02:50:52Z", - "quantity": 249 - }, - { - "startDate": "2023-07-29T02:55:52Z", - "quantity": 251 - }, - { - "startDate": "2023-07-29T03:00:52Z", - "quantity": 250 - }, - { - "startDate": "2023-07-29T03:05:52Z", - "quantity": 253 - }, - { - "startDate": "2023-07-29T03:10:52Z", - "quantity": 253 - }, - { - "startDate": "2023-07-29T03:15:52Z", - "quantity": 253 - }, - { - "startDate": "2023-07-29T03:20:52Z", - "quantity": 248 - }, - { - "startDate": "2023-07-29T03:25:51Z", - "quantity": 254 - }, - { - "startDate": "2023-07-29T03:30:52Z", - "quantity": 253 - }, - { - "startDate": "2023-07-29T03:35:51Z", - "quantity": 244 - }, - { - "startDate": "2023-07-29T03:40:52Z", - "quantity": 239 - }, - { - "startDate": "2023-07-29T03:45:51Z", - "quantity": 234 - }, - { - "startDate": "2023-07-29T03:50:52Z", - "quantity": 225 - }, - { - "startDate": "2023-07-29T03:55:52Z", - "quantity": 220 - }, - { - "startDate": "2023-07-29T04:00:51Z", - "quantity": 213 - }, - { - "startDate": "2023-07-29T04:05:52Z", - "quantity": 211 - }, - { - "startDate": "2023-07-29T04:10:52Z", - "quantity": 209 - }, - { - "startDate": "2023-07-29T04:15:52Z", - "quantity": 201 - }, - { - "startDate": "2023-07-29T04:20:52Z", - "quantity": 193 - }, - { - "startDate": "2023-07-29T04:25:52Z", - "quantity": 190 - }, - { - "startDate": "2023-07-29T04:30:51Z", - "quantity": 180 - }, - { - "startDate": "2023-07-29T04:35:51Z", - "quantity": 154 - }, - { - "startDate": "2023-07-29T04:40:52Z", - "quantity": 118 - }, - { - "startDate": "2023-07-29T04:45:52Z", - "quantity": 110 - }, - { - "startDate": "2023-07-29T04:50:51Z", - "quantity": 114 - }, - { - "startDate": "2023-07-29T04:55:51Z", - "quantity": 120 - }, - { - "startDate": "2023-07-29T05:00:51Z", - "quantity": 109 - }, - { - "startDate": "2023-07-29T05:05:51Z", - "quantity": 104 - }, - { - "startDate": "2023-07-29T05:10:51Z", - "quantity": 101 - }, - { - "startDate": "2023-07-29T05:15:52Z", - "quantity": 95 - }, - { - "startDate": "2023-07-29T05:20:52Z", - "quantity": 86 - }, - { - "startDate": "2023-07-29T05:25:51Z", - "quantity": 82 - }, - { - "startDate": "2023-07-29T05:30:52Z", - "quantity": 77 - }, - { - "startDate": "2023-07-29T05:35:52Z", - "quantity": 72 - }, - { - "startDate": "2023-07-29T05:40:51Z", - "quantity": 68 - }, - { - "startDate": "2023-07-29T05:45:52Z", - "quantity": 67 - }, - { - "startDate": "2023-07-29T05:50:52Z", - "quantity": 59 - }, - { - "startDate": "2023-07-29T05:55:52Z", - "quantity": 62 - }, - { - "startDate": "2023-07-29T06:00:52Z", - "quantity": 64 - }, - { - "startDate": "2023-07-29T06:05:52Z", - "quantity": 64 - }, - { - "startDate": "2023-07-29T06:10:52Z", - "quantity": 66 - }, - { - "startDate": "2023-07-29T06:15:52Z", - "quantity": 67 - }, - { - "startDate": "2023-07-29T06:20:52Z", - "quantity": 67 - }, - { - "startDate": "2023-07-29T06:25:52Z", - "quantity": 72 - }, - { - "startDate": "2023-07-29T06:30:52Z", - "quantity": 90 - }, - { - "startDate": "2023-07-29T06:35:52Z", - "quantity": 125 - }, - { - "startDate": "2023-07-29T06:40:52Z", - "quantity": 134 - }, - { - "startDate": "2023-07-29T06:45:52Z", - "quantity": 145 - }, - { - "startDate": "2023-07-29T06:50:52Z", - "quantity": 169 - }, - { - "startDate": "2023-07-29T06:55:52Z", - "quantity": 183 - }, - { - "startDate": "2023-07-29T07:00:52Z", - "quantity": 228 - }, - { - "startDate": "2023-07-29T07:05:53Z", - "quantity": 257 - }, - { - "startDate": "2023-07-29T07:10:52Z", - "quantity": 250 - }, - { - "startDate": "2023-07-29T07:15:52Z", - "quantity": 232 - }, - { - "startDate": "2023-07-29T07:20:52Z", - "quantity": 225 - }, - { - "startDate": "2023-07-29T07:25:52Z", - "quantity": 241 - }, - { - "startDate": "2023-07-29T07:30:52Z", - "quantity": 233 - }, - { - "startDate": "2023-07-29T07:35:53Z", - "quantity": 227 - }, - { - "startDate": "2023-07-29T07:40:52Z", - "quantity": 222 - }, - { - "startDate": "2023-07-29T07:45:52Z", - "quantity": 216 - }, - { - "startDate": "2023-07-29T07:50:52Z", - "quantity": 211 - }, - { - "startDate": "2023-07-29T07:55:52Z", - "quantity": 208 - }, - { - "startDate": "2023-07-29T08:00:52Z", - "quantity": 207 - }, - { - "startDate": "2023-07-29T08:05:52Z", - "quantity": 209 - }, - { - "startDate": "2023-07-29T08:10:52Z", - "quantity": 200 - }, - { - "startDate": "2023-07-29T08:15:52Z", - "quantity": 193 - }, - { - "startDate": "2023-07-29T08:20:52Z", - "quantity": 186 - }, - { - "startDate": "2023-07-29T08:25:52Z", - "quantity": 176 - }, - { - "startDate": "2023-07-29T08:30:52Z", - "quantity": 169 - }, - { - "startDate": "2023-07-29T08:35:52Z", - "quantity": 166 - }, - { - "startDate": "2023-07-29T08:40:52Z", - "quantity": 170 - }, - { - "startDate": "2023-07-29T08:45:52Z", - "quantity": 161 - }, - { - "startDate": "2023-07-29T08:50:53Z", - "quantity": 150 - }, - { - "startDate": "2023-07-29T08:55:53Z", - "quantity": 144 - }, - { - "startDate": "2023-07-29T09:00:53Z", - "quantity": 136 - }, - { - "startDate": "2023-07-29T09:05:53Z", - "quantity": 128 - }, - { - "startDate": "2023-07-29T09:10:52Z", - "quantity": 127 - }, - { - "startDate": "2023-07-29T09:15:53Z", - "quantity": 126 - }, - { - "startDate": "2023-07-29T09:20:53Z", - "quantity": 121 - }, - { - "startDate": "2023-07-29T09:25:52Z", - "quantity": 117 - }, - { - "startDate": "2023-07-29T09:30:52Z", - "quantity": 116 - }, - { - "startDate": "2023-07-29T09:35:53Z", - "quantity": 115 - }, - { - "startDate": "2023-07-29T09:40:52Z", - "quantity": 113 - }, - { - "startDate": "2023-07-29T09:45:52Z", - "quantity": 113 - }, - { - "startDate": "2023-07-29T09:50:52Z", - "quantity": 109 - }, - { - "startDate": "2023-07-29T09:55:53Z", - "quantity": 114 - }, - { - "startDate": "2023-07-29T10:00:52Z", - "quantity": 115 - }, - { - "startDate": "2023-07-29T10:05:52Z", - "quantity": 114 - }, - { - "startDate": "2023-07-29T10:10:53Z", - "quantity": 111 - }, - { - "startDate": "2023-07-29T10:15:52Z", - "quantity": 112 - }, - { - "startDate": "2023-07-29T10:20:52Z", - "quantity": 113 - }, - { - "startDate": "2023-07-29T10:25:53Z", - "quantity": 114 - }, - { - "startDate": "2023-07-29T10:30:52Z", - "quantity": 113 - }, - { - "startDate": "2023-07-29T10:35:52Z", - "quantity": 113 - }, - { - "startDate": "2023-07-29T10:40:53Z", - "quantity": 111 - }, - { - "startDate": "2023-07-29T10:45:53Z", - "quantity": 110 - }, - { - "startDate": "2023-07-29T10:50:53Z", - "quantity": 111 - }, - { - "startDate": "2023-07-29T10:55:52Z", - "quantity": 112 - }, - { - "startDate": "2023-07-29T11:00:53Z", - "quantity": 112 - }, - { - "startDate": "2023-07-29T11:05:52Z", - "quantity": 111 - }, - { - "startDate": "2023-07-29T11:10:52Z", - "quantity": 109 - }, - { - "startDate": "2023-07-29T11:15:52Z", - "quantity": 107 - }, - { - "startDate": "2023-07-29T11:20:52Z", - "quantity": 104 - }, - { - "startDate": "2023-07-29T11:25:53Z", - "quantity": 104 - }, - { - "startDate": "2023-07-29T11:30:53Z", - "quantity": 104 - }, - { - "startDate": "2023-07-29T11:35:53Z", - "quantity": 97 - }, - { - "startDate": "2023-07-29T11:40:53Z", - "quantity": 98 - }, - { - "startDate": "2023-07-29T11:45:52Z", - "quantity": 98 - }, - { - "startDate": "2023-07-29T11:50:53Z", - "quantity": 99 - }, - { - "startDate": "2023-07-29T11:55:52Z", - "quantity": 97 - }, - { - "startDate": "2023-07-29T12:00:53Z", - "quantity": 97 - }, - { - "startDate": "2023-07-29T12:05:53Z", - "quantity": 97 - }, - { - "startDate": "2023-07-29T12:10:53Z", - "quantity": 97 - }, - { - "startDate": "2023-07-29T12:15:52Z", - "quantity": 97 - }, - { - "startDate": "2023-07-29T12:20:53Z", - "quantity": 99 - }, - { - "startDate": "2023-07-29T12:25:53Z", - "quantity": 96 - }, - { - "startDate": "2023-07-29T12:30:53Z", - "quantity": 95 - }, - { - "startDate": "2023-07-29T12:35:53Z", - "quantity": 95 - }, - { - "startDate": "2023-07-29T12:40:53Z", - "quantity": 94 - }, - { - "startDate": "2023-07-29T12:45:53Z", - "quantity": 89 - }, - { - "startDate": "2023-07-29T12:50:53Z", - "quantity": 89 - }, - { - "startDate": "2023-07-29T12:55:53Z", - "quantity": 91 - }, - { - "startDate": "2023-07-29T13:00:52Z", - "quantity": 86 - }, - { - "startDate": "2023-07-29T13:05:53Z", - "quantity": 83 - }, - { - "startDate": "2023-07-29T13:10:53Z", - "quantity": 86 - }, - { - "startDate": "2023-07-29T13:15:53Z", - "quantity": 84 - }, - { - "startDate": "2023-07-29T13:20:53Z", - "quantity": 81 - }, - { - "startDate": "2023-07-29T13:25:53Z", - "quantity": 80 - }, - { - "startDate": "2023-07-29T13:30:53Z", - "quantity": 78 - }, - { - "startDate": "2023-07-29T13:35:53Z", - "quantity": 78 - }, - { - "startDate": "2023-07-29T13:40:53Z", - "quantity": 74 - }, - { - "startDate": "2023-07-29T13:45:53Z", - "quantity": 72 - }, - { - "startDate": "2023-07-29T13:50:52Z", - "quantity": 72 - }, - { - "startDate": "2023-07-29T13:55:53Z", - "quantity": 71 - }, - { - "startDate": "2023-07-29T14:00:53Z", - "quantity": 70 - }, - { - "startDate": "2023-07-29T14:05:53Z", - "quantity": 69 - }, - { - "startDate": "2023-07-29T14:10:53Z", - "quantity": 72 - }, - { - "startDate": "2023-07-29T14:15:53Z", - "quantity": 75 - }, - { - "startDate": "2023-07-29T14:20:53Z", - "quantity": 78 - }, - { - "startDate": "2023-07-29T14:25:53Z", - "quantity": 83 - }, - { - "startDate": "2023-07-29T14:30:53Z", - "quantity": 85 - }, - { - "startDate": "2023-07-29T14:35:53Z", - "quantity": 88 - }, - { - "startDate": "2023-07-29T14:40:53Z", - "quantity": 98 - }, - { - "startDate": "2023-07-29T14:45:53Z", - "quantity": 94 - }, - { - "startDate": "2023-07-29T14:50:53Z", - "quantity": 93 - }, - { - "startDate": "2023-07-29T14:55:53Z", - "quantity": 113 - }, - { - "startDate": "2023-07-29T15:00:53Z", - "quantity": 128 - }, - { - "startDate": "2023-07-29T15:05:53Z", - "quantity": 126 - }, - { - "startDate": "2023-07-29T15:10:54Z", - "quantity": 148 - }, - { - "startDate": "2023-07-29T15:15:54Z", - "quantity": 161 - }, - { - "startDate": "2023-07-29T15:20:54Z", - "quantity": 157 - }, - { - "startDate": "2023-07-29T15:25:53Z", - "quantity": 151 - }, - { - "startDate": "2023-07-29T15:30:53Z", - "quantity": 180 - }, - { - "startDate": "2023-07-29T15:35:53Z", - "quantity": 203 - }, - { - "startDate": "2023-07-29T15:40:54Z", - "quantity": 196 - }, - { - "startDate": "2023-07-29T15:45:54Z", - "quantity": 225 - }, - { - "startDate": "2023-07-29T15:50:53Z", - "quantity": 248 - }, - { - "startDate": "2023-07-29T15:55:53Z", - "quantity": 245 - }, - { - "startDate": "2023-07-29T16:00:53Z", - "quantity": 249 - }, - { - "startDate": "2023-07-29T16:05:53Z", - "quantity": 248 - }, - { - "startDate": "2023-07-29T16:10:54Z", - "quantity": 267 - }, - { - "startDate": "2023-07-29T16:15:53Z", - "quantity": 266 - }, - { - "startDate": "2023-07-29T16:20:54Z", - "quantity": 259 - }, - { - "startDate": "2023-07-29T16:25:54Z", - "quantity": 259 - }, - { - "startDate": "2023-07-29T16:30:53Z", - "quantity": 246 - }, - { - "startDate": "2023-07-29T16:35:54Z", - "quantity": 228 - }, - { - "startDate": "2023-07-29T16:40:53Z", - "quantity": 232 - }, - { - "startDate": "2023-07-29T16:45:53Z", - "quantity": 244 - }, - { - "startDate": "2023-07-29T16:50:53Z", - "quantity": 233 - }, - { - "startDate": "2023-07-29T16:55:53Z", - "quantity": 218 - }, - { - "startDate": "2023-07-29T17:00:53Z", - "quantity": 212 - }, - { - "startDate": "2023-07-29T17:05:53Z", - "quantity": 206 - }, - { - "startDate": "2023-07-29T17:10:53Z", - "quantity": 191 - }, - { - "startDate": "2023-07-29T17:15:54Z", - "quantity": 170 - }, - { - "startDate": "2023-07-29T17:20:54Z", - "quantity": 164 - }, - { - "startDate": "2023-07-29T17:25:53Z", - "quantity": 161 - }, - { - "startDate": "2023-07-29T17:30:53Z", - "quantity": 162 - }, - { - "startDate": "2023-07-29T17:35:53Z", - "quantity": 140 - }, - { - "startDate": "2023-07-29T17:40:54Z", - "quantity": 110 - }, - { - "startDate": "2023-07-29T17:45:54Z", - "quantity": 106 - }, - { - "startDate": "2023-07-29T17:50:53Z", - "quantity": 107 - }, - { - "startDate": "2023-07-29T17:55:53Z", - "quantity": 105 - }, - { - "startDate": "2023-07-29T18:00:53Z", - "quantity": 106 - }, - { - "startDate": "2023-07-29T18:05:54Z", - "quantity": 111 - }, - { - "startDate": "2023-07-29T18:10:53Z", - "quantity": 115 - }, - { - "startDate": "2023-07-29T18:15:53Z", - "quantity": 109 - }, - { - "startDate": "2023-07-29T18:20:54Z", - "quantity": 117 - }, - { - "startDate": "2023-07-29T18:25:54Z", - "quantity": 135 - }, - { - "startDate": "2023-07-29T18:30:53Z", - "quantity": 120 - }, - { - "startDate": "2023-07-29T18:35:53Z", - "quantity": 117 - }, - { - "startDate": "2023-07-29T18:40:53Z", - "quantity": 116 - }, - { - "startDate": "2023-07-29T18:45:53Z", - "quantity": 125 - }, - { - "startDate": "2023-07-29T18:50:53Z", - "quantity": 131 - }, - { - "startDate": "2023-07-29T18:55:53Z", - "quantity": 136 - }, - { - "startDate": "2023-07-29T19:00:53Z", - "quantity": 142 - }, - { - "startDate": "2023-07-29T19:05:53Z", - "quantity": 152 - }, - { - "startDate": "2023-07-29T19:10:54Z", - "quantity": 159 - }, - { - "startDate": "2023-07-29T19:15:54Z", - "quantity": 176 - }, - { - "startDate": "2023-07-29T19:20:54Z", - "quantity": 170 - } -] diff --git a/LoopTests/Fixtures/live_capture/live_capture_input.json b/LoopTests/Fixtures/live_capture/live_capture_input.json new file mode 100644 index 0000000000..f010194a63 --- /dev/null +++ b/LoopTests/Fixtures/live_capture/live_capture_input.json @@ -0,0 +1,1009 @@ +{ + "carbEntries" : [ + { + "absorptionTime" : 10800, + "quantity" : 22, + "startDate" : "2023-06-22T19:20:53Z" + }, + { + "absorptionTime" : 10800, + "quantity" : 75, + "startDate" : "2023-06-22T21:04:45Z" + }, + { + "absorptionTime" : 10800, + "quantity" : 47, + "startDate" : "2023-06-23T02:10:13Z" + } + ], + "doses" : [ + { + "endDate" : "2023-06-22T16:22:40Z", + "startDate" : "2023-06-22T16:12:40Z", + "type" : "basal", + "unit" : "U", + "value" : 0.050000000000000003 + }, + { + "endDate" : "2023-06-22T16:17:54Z", + "startDate" : "2023-06-22T16:17:46Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.20000000000000001 + }, + { + "endDate" : "2023-06-22T16:32:40Z", + "startDate" : "2023-06-22T16:22:40Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-06-22T16:47:39Z", + "startDate" : "2023-06-22T16:32:40Z", + "type" : "basal", + "unit" : "U", + "value" : 0.10000000000000001 + }, + { + "endDate" : "2023-06-22T16:57:41Z", + "startDate" : "2023-06-22T16:47:39Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-06-22T17:02:38Z", + "startDate" : "2023-06-22T16:57:41Z", + "type" : "basal", + "unit" : "U", + "value" : 0.050000000000000003 + }, + { + "endDate" : "2023-06-22T17:07:38Z", + "startDate" : "2023-06-22T17:02:38Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0.050000000000000003 + }, + { + "endDate" : "2023-06-22T17:22:45Z", + "startDate" : "2023-06-22T17:07:38Z", + "type" : "basal", + "unit" : "U", + "value" : 0.10000000000000001 + }, + { + "endDate" : "2023-06-22T17:12:46Z", + "startDate" : "2023-06-22T17:12:42Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.10000000000000001 + }, + { + "endDate" : "2023-06-22T17:27:39Z", + "startDate" : "2023-06-22T17:22:45Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-06-22T17:27:39Z", + "startDate" : "2023-06-22T17:27:39Z", + "type" : "basal", + "unit" : "U", + "value" : 0 + }, + { + "endDate" : "2023-06-22T17:32:39Z", + "startDate" : "2023-06-22T17:27:39Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0.050000000000000003 + }, + { + "endDate" : "2023-06-22T18:07:38Z", + "startDate" : "2023-06-22T17:32:39Z", + "type" : "basal", + "unit" : "U", + "value" : 0.25 + }, + { + "endDate" : "2023-06-22T17:32:45Z", + "startDate" : "2023-06-22T17:32:41Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.10000000000000001 + }, + { + "endDate" : "2023-06-22T17:42:40Z", + "startDate" : "2023-06-22T17:42:38Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.050000000000000003 + }, + { + "endDate" : "2023-06-22T17:47:43Z", + "startDate" : "2023-06-22T17:47:39Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.10000000000000001 + }, + { + "endDate" : "2023-06-22T18:12:38Z", + "startDate" : "2023-06-22T18:07:38Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-06-22T19:17:40Z", + "startDate" : "2023-06-22T18:12:38Z", + "type" : "basal", + "unit" : "U", + "value" : 0.45000000000000001 + }, + { + "endDate" : "2023-06-22T19:02:43Z", + "startDate" : "2023-06-22T19:02:39Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.10000000000000001 + }, + { + "endDate" : "2023-06-22T19:22:43Z", + "startDate" : "2023-06-22T19:17:40Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-06-22T19:21:49Z", + "startDate" : "2023-06-22T19:21:01Z", + "type" : "bolus", + "unit" : "U", + "value" : 1.2 + }, + { + "endDate" : "2023-06-22T19:37:37Z", + "startDate" : "2023-06-22T19:22:43Z", + "type" : "basal", + "unit" : "U", + "value" : 0.10000000000000001 + }, + { + "endDate" : "2023-06-22T19:27:43Z", + "startDate" : "2023-06-22T19:27:39Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.10000000000000001 + }, + { + "endDate" : "2023-06-22T19:57:48Z", + "startDate" : "2023-06-22T19:37:37Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-06-22T19:57:48Z", + "startDate" : "2023-06-22T19:57:48Z", + "type" : "basal", + "unit" : "U", + "value" : 0 + }, + { + "endDate" : "2023-06-22T20:02:39Z", + "startDate" : "2023-06-22T19:57:48Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-06-22T20:07:40Z", + "startDate" : "2023-06-22T20:02:39Z", + "type" : "basal", + "unit" : "U", + "value" : 0.050000000000000003 + }, + { + "endDate" : "2023-06-22T20:12:40Z", + "startDate" : "2023-06-22T20:07:40Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0.10000000000000001 + }, + { + "endDate" : "2023-06-22T20:52:45Z", + "startDate" : "2023-06-22T20:12:40Z", + "type" : "basal", + "unit" : "U", + "value" : 0.25 + }, + { + "endDate" : "2023-06-22T21:07:43Z", + "startDate" : "2023-06-22T20:52:45Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-06-22T21:07:49Z", + "startDate" : "2023-06-22T21:04:51Z", + "type" : "bolus", + "unit" : "U", + "value" : 4.4500000000000002 + }, + { + "endDate" : "2023-06-22T21:47:38Z", + "startDate" : "2023-06-22T21:07:43Z", + "type" : "basal", + "unit" : "U", + "value" : 0.25 + }, + { + "endDate" : "2023-06-22T21:12:42Z", + "startDate" : "2023-06-22T21:12:40Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.050000000000000003 + }, + { + "endDate" : "2023-06-22T22:07:39Z", + "startDate" : "2023-06-22T21:47:38Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-06-22T23:42:40Z", + "startDate" : "2023-06-22T22:07:39Z", + "type" : "basal", + "unit" : "U", + "value" : 0.65000000000000002 + }, + { + "endDate" : "2023-06-22T22:27:46Z", + "startDate" : "2023-06-22T22:27:38Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.20000000000000001 + }, + { + "endDate" : "2023-06-22T22:37:44Z", + "startDate" : "2023-06-22T22:37:40Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.10000000000000001 + }, + { + "endDate" : "2023-06-22T22:42:42Z", + "startDate" : "2023-06-22T22:42:40Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.050000000000000003 + }, + { + "endDate" : "2023-06-22T23:52:44Z", + "startDate" : "2023-06-22T23:42:40Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-06-22T23:57:46Z", + "startDate" : "2023-06-22T23:52:44Z", + "type" : "basal", + "unit" : "U", + "value" : 0.050000000000000003 + }, + { + "endDate" : "2023-06-23T00:02:37Z", + "startDate" : "2023-06-22T23:57:46Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0.050000000000000003 + }, + { + "endDate" : "2023-06-23T01:02:52Z", + "startDate" : "2023-06-23T00:02:37Z", + "type" : "basal", + "unit" : "U", + "value" : 0.40000000000000002 + }, + { + "endDate" : "2023-06-23T00:07:42Z", + "startDate" : "2023-06-23T00:07:40Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.050000000000000003 + }, + { + "endDate" : "2023-06-23T00:12:44Z", + "startDate" : "2023-06-23T00:12:38Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.14999999999999999 + }, + { + "endDate" : "2023-06-23T00:22:43Z", + "startDate" : "2023-06-23T00:22:39Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.10000000000000001 + }, + { + "endDate" : "2023-06-23T00:27:49Z", + "startDate" : "2023-06-23T00:27:41Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.20000000000000001 + }, + { + "endDate" : "2023-06-23T00:32:43Z", + "startDate" : "2023-06-23T00:32:39Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.10000000000000001 + }, + { + "endDate" : "2023-06-23T00:37:58Z", + "startDate" : "2023-06-23T00:37:48Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.25 + }, + { + "endDate" : "2023-06-23T00:42:47Z", + "startDate" : "2023-06-23T00:42:39Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.20000000000000001 + }, + { + "endDate" : "2023-06-23T00:47:44Z", + "startDate" : "2023-06-23T00:47:40Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.10000000000000001 + }, + { + "endDate" : "2023-06-23T00:52:51Z", + "startDate" : "2023-06-23T00:52:45Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.14999999999999999 + }, + { + "endDate" : "2023-06-23T01:12:49Z", + "startDate" : "2023-06-23T01:02:52Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-06-23T01:17:41Z", + "startDate" : "2023-06-23T01:12:49Z", + "type" : "basal", + "unit" : "U", + "value" : 0.050000000000000003 + }, + { + "endDate" : "2023-06-23T01:12:54Z", + "startDate" : "2023-06-23T01:12:50Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.10000000000000001 + }, + { + "endDate" : "2023-06-23T01:37:39Z", + "startDate" : "2023-06-23T01:17:41Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-06-23T01:37:39Z", + "startDate" : "2023-06-23T01:37:39Z", + "type" : "basal", + "unit" : "U", + "value" : 0 + }, + { + "endDate" : "2023-06-23T01:42:38Z", + "startDate" : "2023-06-23T01:37:39Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-06-23T02:07:42Z", + "startDate" : "2023-06-23T01:42:38Z", + "type" : "basal", + "unit" : "U", + "value" : 0.14999999999999999 + }, + { + "endDate" : "2023-06-23T01:47:46Z", + "startDate" : "2023-06-23T01:47:38Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.20000000000000001 + }, + { + "endDate" : "2023-06-23T01:52:47Z", + "startDate" : "2023-06-23T01:52:39Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.20000000000000001 + }, + { + "endDate" : "2023-06-23T01:57:50Z", + "startDate" : "2023-06-23T01:57:40Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.25 + }, + { + "endDate" : "2023-06-23T02:02:49Z", + "startDate" : "2023-06-23T02:02:39Z", + "type" : "bolus", + "unit" : "U", + "value" : 0.25 + }, + { + "endDate" : "2023-06-23T02:07:36Z", + "startDate" : "2023-06-23T02:04:30Z", + "type" : "bolus", + "unit" : "U", + "value" : 4.6500000000000004 + }, + { + "endDate" : "2023-06-23T02:27:44Z", + "startDate" : "2023-06-23T02:07:42Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + }, + { + "endDate" : "2023-06-23T02:27:44Z", + "startDate" : "2023-06-23T02:27:44Z", + "type" : "basal", + "unit" : "U", + "value" : 0 + }, + { + "endDate" : "2023-06-23T02:47:39Z", + "startDate" : "2023-06-23T02:27:44Z", + "type" : "tempBasal", + "unit" : "U\/hour", + "value" : 0 + } + ], + "glucoseHistory" : [ + { + "quantity" : 120, + "startDate" : "2023-06-22T16:42:33Z" + }, + { + "quantity" : 119, + "startDate" : "2023-06-22T16:47:33Z" + }, + { + "quantity" : 120, + "startDate" : "2023-06-22T16:52:34Z" + }, + { + "quantity" : 118, + "startDate" : "2023-06-22T16:57:34Z" + }, + { + "quantity" : 115, + "startDate" : "2023-06-22T17:02:34Z" + }, + { + "quantity" : 120, + "startDate" : "2023-06-22T17:07:34Z" + }, + { + "quantity" : 121, + "startDate" : "2023-06-22T17:12:34Z" + }, + { + "quantity" : 119, + "startDate" : "2023-06-22T17:17:34Z" + }, + { + "quantity" : 116, + "startDate" : "2023-06-22T17:22:34Z" + }, + { + "quantity" : 115, + "startDate" : "2023-06-22T17:27:34Z" + }, + { + "quantity" : 124, + "startDate" : "2023-06-22T17:32:34Z" + }, + { + "quantity" : 114, + "startDate" : "2023-06-22T17:37:34Z" + }, + { + "quantity" : 124, + "startDate" : "2023-06-22T17:42:34Z" + }, + { + "quantity" : 124, + "startDate" : "2023-06-22T17:47:33Z" + }, + { + "quantity" : 124, + "startDate" : "2023-06-22T17:52:34Z" + }, + { + "quantity" : 126, + "startDate" : "2023-06-22T17:57:33Z" + }, + { + "quantity" : 125, + "startDate" : "2023-06-22T18:02:34Z" + }, + { + "quantity" : 118, + "startDate" : "2023-06-22T18:07:34Z" + }, + { + "quantity" : 122, + "startDate" : "2023-06-22T18:12:33Z" + }, + { + "quantity" : 123, + "startDate" : "2023-06-22T18:17:34Z" + }, + { + "quantity" : 123, + "startDate" : "2023-06-22T18:22:34Z" + }, + { + "quantity" : 121, + "startDate" : "2023-06-22T18:27:34Z" + }, + { + "quantity" : 118, + "startDate" : "2023-06-22T18:32:34Z" + }, + { + "quantity" : 116, + "startDate" : "2023-06-22T18:37:34Z" + }, + { + "quantity" : 118, + "startDate" : "2023-06-22T18:42:34Z" + }, + { + "quantity" : 115, + "startDate" : "2023-06-22T18:47:34Z" + }, + { + "quantity" : 117, + "startDate" : "2023-06-22T18:52:34Z" + }, + { + "quantity" : 125, + "startDate" : "2023-06-22T18:57:34Z" + }, + { + "quantity" : 122, + "startDate" : "2023-06-22T19:02:34Z" + }, + { + "quantity" : 119, + "startDate" : "2023-06-22T19:07:34Z" + }, + { + "quantity" : 120, + "startDate" : "2023-06-22T19:12:34Z" + }, + { + "quantity" : 112, + "startDate" : "2023-06-22T19:17:34Z" + }, + { + "quantity" : 111, + "startDate" : "2023-06-22T19:22:34Z" + }, + { + "quantity" : 114, + "startDate" : "2023-06-22T19:27:34Z" + }, + { + "quantity" : 117, + "startDate" : "2023-06-22T19:32:34Z" + }, + { + "quantity" : 107, + "startDate" : "2023-06-22T19:37:34Z" + }, + { + "quantity" : 113, + "startDate" : "2023-06-22T19:42:34Z" + }, + { + "quantity" : 117, + "startDate" : "2023-06-22T19:47:34Z" + }, + { + "quantity" : 109, + "startDate" : "2023-06-22T19:52:34Z" + }, + { + "quantity" : 117, + "startDate" : "2023-06-22T19:57:34Z" + }, + { + "quantity" : 121, + "startDate" : "2023-06-22T20:02:34Z" + }, + { + "quantity" : 121, + "startDate" : "2023-06-22T20:07:34Z" + }, + { + "quantity" : 127, + "startDate" : "2023-06-22T20:12:34Z" + }, + { + "quantity" : 133, + "startDate" : "2023-06-22T20:17:34Z" + }, + { + "quantity" : 131, + "startDate" : "2023-06-22T20:22:34Z" + }, + { + "quantity" : 132, + "startDate" : "2023-06-22T20:27:34Z" + }, + { + "quantity" : 134, + "startDate" : "2023-06-22T20:32:34Z" + }, + { + "quantity" : 134, + "startDate" : "2023-06-22T20:37:34Z" + }, + { + "quantity" : 139, + "startDate" : "2023-06-22T20:42:34Z" + }, + { + "quantity" : 139, + "startDate" : "2023-06-22T20:47:34Z" + }, + { + "quantity" : 132, + "startDate" : "2023-06-22T20:52:34Z" + }, + { + "quantity" : 118, + "startDate" : "2023-06-22T20:57:34Z" + }, + { + "quantity" : 123, + "startDate" : "2023-06-22T21:02:34Z" + }, + { + "quantity" : 122, + "startDate" : "2023-06-22T21:07:34Z" + }, + { + "quantity" : 119, + "startDate" : "2023-06-22T21:12:34Z" + }, + { + "quantity" : 116, + "startDate" : "2023-06-22T21:17:34Z" + }, + { + "quantity" : 113, + "startDate" : "2023-06-22T21:22:34Z" + }, + { + "quantity" : 111, + "startDate" : "2023-06-22T21:27:34Z" + }, + { + "quantity" : 112, + "startDate" : "2023-06-22T21:32:34Z" + }, + { + "quantity" : 107, + "startDate" : "2023-06-22T21:37:34Z" + }, + { + "quantity" : 102, + "startDate" : "2023-06-22T21:42:34Z" + }, + { + "quantity" : 95, + "startDate" : "2023-06-22T21:47:34Z" + }, + { + "quantity" : 96, + "startDate" : "2023-06-22T21:52:34Z" + }, + { + "quantity" : 89, + "startDate" : "2023-06-22T21:57:34Z" + }, + { + "quantity" : 95, + "startDate" : "2023-06-22T22:02:34Z" + }, + { + "quantity" : 95, + "startDate" : "2023-06-22T22:07:34Z" + }, + { + "quantity" : 93, + "startDate" : "2023-06-22T22:12:34Z" + }, + { + "quantity" : 98, + "startDate" : "2023-06-22T22:17:35Z" + }, + { + "quantity" : 95, + "startDate" : "2023-06-22T22:22:35Z" + }, + { + "quantity" : 101, + "startDate" : "2023-06-22T22:27:34Z" + }, + { + "quantity" : 97, + "startDate" : "2023-06-22T22:32:34Z" + }, + { + "quantity" : 108, + "startDate" : "2023-06-22T22:37:35Z" + }, + { + "quantity" : 109, + "startDate" : "2023-06-22T22:42:34Z" + }, + { + "quantity" : 109, + "startDate" : "2023-06-22T22:47:34Z" + }, + { + "quantity" : 114, + "startDate" : "2023-06-22T22:52:34Z" + }, + { + "quantity" : 115, + "startDate" : "2023-06-22T22:57:34Z" + }, + { + "quantity" : 114, + "startDate" : "2023-06-22T23:02:34Z" + }, + { + "quantity" : 121, + "startDate" : "2023-06-22T23:07:34Z" + }, + { + "quantity" : 119, + "startDate" : "2023-06-22T23:12:34Z" + }, + { + "quantity" : 117, + "startDate" : "2023-06-22T23:17:34Z" + }, + { + "quantity" : 120, + "startDate" : "2023-06-22T23:22:35Z" + }, + { + "quantity" : 122, + "startDate" : "2023-06-22T23:27:34Z" + }, + { + "quantity" : 123, + "startDate" : "2023-06-22T23:32:34Z" + }, + { + "quantity" : 127, + "startDate" : "2023-06-22T23:37:34Z" + }, + { + "quantity" : 118, + "startDate" : "2023-06-22T23:42:35Z" + }, + { + "quantity" : 120, + "startDate" : "2023-06-22T23:47:34Z" + }, + { + "quantity" : 119, + "startDate" : "2023-06-22T23:52:35Z" + }, + { + "quantity" : 115, + "startDate" : "2023-06-22T23:57:34Z" + }, + { + "quantity" : 116, + "startDate" : "2023-06-23T00:02:34Z" + }, + { + "quantity" : 133, + "startDate" : "2023-06-23T00:07:34Z" + }, + { + "quantity" : 145, + "startDate" : "2023-06-23T00:12:34Z" + }, + { + "quantity" : 140, + "startDate" : "2023-06-23T00:17:34Z" + }, + { + "quantity" : 161, + "startDate" : "2023-06-23T00:22:35Z" + }, + { + "quantity" : 166, + "startDate" : "2023-06-23T00:27:34Z" + }, + { + "quantity" : 172, + "startDate" : "2023-06-23T00:32:35Z" + }, + { + "quantity" : 182, + "startDate" : "2023-06-23T00:37:35Z" + }, + { + "quantity" : 184, + "startDate" : "2023-06-23T00:42:35Z" + }, + { + "quantity" : 185, + "startDate" : "2023-06-23T00:47:34Z" + }, + { + "quantity" : 190, + "startDate" : "2023-06-23T00:52:35Z" + }, + { + "quantity" : 182, + "startDate" : "2023-06-23T00:57:34Z" + }, + { + "quantity" : 166, + "startDate" : "2023-06-23T01:02:35Z" + }, + { + "quantity" : 174, + "startDate" : "2023-06-23T01:07:34Z" + }, + { + "quantity" : 179, + "startDate" : "2023-06-23T01:12:34Z" + }, + { + "quantity" : 166, + "startDate" : "2023-06-23T01:17:35Z" + }, + { + "quantity" : 134, + "startDate" : "2023-06-23T01:22:34Z" + }, + { + "quantity" : 131, + "startDate" : "2023-06-23T01:27:35Z" + }, + { + "quantity" : 129, + "startDate" : "2023-06-23T01:32:34Z" + }, + { + "quantity" : 136, + "startDate" : "2023-06-23T01:37:34Z" + }, + { + "quantity" : 152, + "startDate" : "2023-06-23T01:42:34Z" + }, + { + "quantity" : 162, + "startDate" : "2023-06-23T01:47:35Z" + }, + { + "quantity" : 165, + "startDate" : "2023-06-23T01:52:34Z" + }, + { + "quantity" : 172, + "startDate" : "2023-06-23T01:57:34Z" + }, + { + "quantity" : 176, + "startDate" : "2023-06-23T02:02:35Z" + }, + { + "quantity" : 165, + "startDate" : "2023-06-23T02:07:35Z" + }, + { + "quantity" : 172, + "startDate" : "2023-06-23T02:12:34Z" + }, + { + "quantity" : 170, + "startDate" : "2023-06-23T02:17:35Z" + }, + { + "quantity" : 177, + "startDate" : "2023-06-23T02:22:35Z" + }, + { + "quantity" : 176, + "startDate" : "2023-06-23T02:27:35Z" + }, + { + "quantity" : 173, + "startDate" : "2023-06-23T02:32:34Z" + }, + { + "quantity" : 180, + "startDate" : "2023-06-23T02:37:35Z" + } + ], + "settings" : { + "basal" : [ + { + "endDate" : "2023-06-23T05:00:00Z", + "startDate" : "2023-06-22T10:00:00Z", + "value" : 0.45000000000000001 + } + ], + "carbRatio" : [ + { + "endDate" : "2023-06-23T07:00:00Z", + "startDate" : "2023-06-22T07:00:00Z", + "value" : 11 + } + ], + "maximumBasalRatePerHour" : null, + "maximumBolus" : null, + "sensitivity" : [ + { + "endDate" : "2023-06-23T05:00:00Z", + "startDate" : "2023-06-22T10:00:00Z", + "value" : 60 + } + ], + "suspendThreshold" : null, + "target" : [ + { + "endDate" : "2023-06-23T07:00:00Z", + "startDate" : "2023-06-22T20:25:00Z", + "value" : { + "maxValue" : 115, + "minValue" : 100 + } + }, + { + "endDate" : "2023-06-23T08:50:00Z", + "startDate" : "2023-06-23T07:00:00Z", + "value" : { + "maxValue" : 115, + "minValue" : 100 + } + } + ] + } +} diff --git a/LoopTests/Fixtures/live_capture/live_capture_predicted_glucose.json b/LoopTests/Fixtures/live_capture/live_capture_predicted_glucose.json index 8ec8f4688c..a98fbaccb7 100644 --- a/LoopTests/Fixtures/live_capture/live_capture_predicted_glucose.json +++ b/LoopTests/Fixtures/live_capture/live_capture_predicted_glucose.json @@ -1,322 +1,392 @@ [ { + "quantity" : 180, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T19:20:54Z", - "quantity" : 170 + "startDate" : "2023-06-23T02:37:35Z" }, { + "quantity" : 180.29132150657966, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T19:25:00Z", - "quantity" : 174.50999999999999 + "startDate" : "2023-06-23T02:40:00Z" }, { + "quantity" : 180.51458820506667, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T19:30:00Z", - "quantity" : 177.56455527893289 + "startDate" : "2023-06-23T02:45:00Z" }, { + "quantity" : 179.7158986124237, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T19:35:00Z", - "quantity" : 177.67338252385397 + "startDate" : "2023-06-23T02:50:00Z" }, { + "quantity" : 177.66868460973922, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T19:40:00Z", - "quantity" : 174.91272423228034 + "startDate" : "2023-06-23T02:55:00Z" }, { + "quantity" : 174.80252509117634, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T19:45:00Z", - "quantity" : 171.75695070238604 + "startDate" : "2023-06-23T03:00:00Z" }, { + "quantity" : 171.74984493231631, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T19:50:00Z", - "quantity" : 168.76244616110549 + "startDate" : "2023-06-23T03:05:00Z" }, { + "quantity" : 168.58187755437024, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T19:55:00Z", - "quantity" : 165.95427643567874 + "startDate" : "2023-06-23T03:10:00Z" }, { + "quantity" : 165.36216340804185, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T20:00:00Z", - "quantity" : 163.35383111783804 + "startDate" : "2023-06-23T03:15:00Z" }, { + "quantity" : 162.12697210734922, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T20:05:00Z", - "quantity" : 160.97916167823055 + "startDate" : "2023-06-23T03:20:00Z" }, { + "quantity" : 158.90986429144345, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T20:10:00Z", - "quantity" : 158.84529353872236 + "startDate" : "2023-06-23T03:25:00Z" }, { + "quantity" : 155.75684851046043, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T20:15:00Z", - "quantity" : 156.96451392177406 + "startDate" : "2023-06-23T03:30:00Z" }, { + "quantity" : 152.70869296700107, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T20:20:00Z", - "quantity" : 155.34663717673348 + "startDate" : "2023-06-23T03:35:00Z" }, { + "quantity" : 149.78068888956841, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T20:25:00Z", - "quantity" : 153.99924917105037 + "startDate" : "2023-06-23T03:40:00Z" }, { + "quantity" : 147.00401242102828, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T20:30:00Z", - "quantity" : 152.92793222961055 + "startDate" : "2023-06-23T03:45:00Z" }, { + "quantity" : 144.40563853768242, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T20:35:00Z", - "quantity" : 152.13647200718029 + "startDate" : "2023-06-23T03:50:00Z" }, { + "quantity" : 142.0087170601098, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T20:40:00Z", - "quantity" : 151.62704758695727 + "startDate" : "2023-06-23T03:55:00Z" }, { + "quantity" : 139.83295658233396, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T20:45:00Z", - "quantity" : 151.36168828514261 + "startDate" : "2023-06-23T04:00:00Z" }, { + "quantity" : 137.89511837124121, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T20:50:00Z", - "quantity" : 151.16659478660213 + "startDate" : "2023-06-23T04:05:00Z" }, { + "quantity" : 136.07526338088792, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T20:55:00Z", - "quantity" : 151.01925161349905 + "startDate" : "2023-06-23T04:10:00Z" }, { + "quantity" : 134.25815754225141, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T21:00:00Z", - "quantity" : 150.9170176497727 + "startDate" : "2023-06-23T04:15:00Z" }, { + "quantity" : 132.45275084533137, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T21:05:00Z", - "quantity" : 150.85638361169495 + "startDate" : "2023-06-23T04:20:00Z" }, { + "quantity" : 130.66563522056958, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T21:10:00Z", - "quantity" : 150.83308419477353 + "startDate" : "2023-06-23T04:25:00Z" }, { + "quantity" : 128.90146920949769, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T21:15:00Z", - "quantity" : 150.84043373508726 + "startDate" : "2023-06-23T04:30:00Z" }, { + "quantity" : 127.16322092092855, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T21:20:00Z", - "quantity" : 150.86809756991411 + "startDate" : "2023-06-23T04:35:00Z" }, { + "quantity" : 125.45215396105368, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T21:25:00Z", - "quantity" : 150.9067680932084 + "startDate" : "2023-06-23T04:40:00Z" }, { + "quantity" : 123.76712483433676, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T21:30:00Z", - "quantity" : 150.94864870532584 + "startDate" : "2023-06-23T04:45:00Z" }, { + "quantity" : 122.10683165409341, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T21:35:00Z", - "quantity" : 150.98521164138711 + "startDate" : "2023-06-23T04:50:00Z" }, { + "quantity" : 120.46857875163471, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T21:40:00Z", - "quantity" : 151.00885216396446 + "startDate" : "2023-06-23T04:55:00Z" }, { + "quantity" : 118.84903308222181, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T21:45:00Z", - "quantity" : 151.01338818778473 + "startDate" : "2023-06-23T05:00:00Z" }, { + "quantity" : 117.24445077397047, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T21:50:00Z", - "quantity" : 150.99143437519743 + "startDate" : "2023-06-23T05:05:00Z" }, { + "quantity" : 115.65043839655846, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T21:55:00Z", - "quantity" : 150.93296955721272 + "startDate" : "2023-06-23T05:10:00Z" }, { + "quantity" : 114.06198688414838, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T22:00:00Z", - "quantity" : 150.82844495083614 + "startDate" : "2023-06-23T05:15:00Z" }, { + "quantity" : 112.47356001340279, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T22:05:00Z", - "quantity" : 150.67007248833207 + "startDate" : "2023-06-23T05:20:00Z" }, { + "quantity" : 110.87917488553444, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T22:10:00Z", - "quantity" : 150.44818289417267 + "startDate" : "2023-06-23T05:25:00Z" }, { + "quantity" : 109.27247502015473, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T22:15:00Z", - "quantity" : 150.15333032546724 + "startDate" : "2023-06-23T05:30:00Z" }, { + "quantity" : 107.64679662666447, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T22:20:00Z", - "quantity" : 149.77853346820271 + "startDate" : "2023-06-23T05:35:00Z" }, { + "quantity" : 105.99522857963143, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T22:25:00Z", - "quantity" : 149.31770786853204 + "startDate" : "2023-06-23T05:40:00Z" }, { + "quantity" : 104.31066658787131, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T22:30:00Z", - "quantity" : 148.76463863907512 + "startDate" : "2023-06-23T05:45:00Z" }, { + "quantity" : 102.58586201263279, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T22:35:00Z", - "quantity" : 148.11209303652518 + "startDate" : "2023-06-23T05:50:00Z" }, { + "quantity" : 100.81350120847731, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T22:40:00Z", - "quantity" : 147.3537767195518 + "startDate" : "2023-06-23T05:55:00Z" }, { + "quantity" : 98.986445102805988, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T22:45:00Z", - "quantity" : 146.48451003000551 + "startDate" : "2023-06-23T06:00:00Z" }, { + "quantity" : 97.097518927124952, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T22:50:00Z", - "quantity" : 145.49919962625873 + "startDate" : "2023-06-23T06:05:00Z" }, { + "quantity" : 95.139330662672023, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T22:55:00Z", - "quantity" : 144.39282841481932 + "startDate" : "2023-06-23T06:10:00Z" }, { + "quantity" : 93.104670202578632, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T23:00:00Z", - "quantity" : 143.1991839953188 + "startDate" : "2023-06-23T06:15:00Z" }, { + "quantity" : 90.986165185301502, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T23:05:00Z", - "quantity" : 142.08670682751759 + "startDate" : "2023-06-23T06:20:00Z" }, { + "quantity" : 88.909927040807588, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T23:10:00Z", - "quantity" : 141.07153230327131 + "startDate" : "2023-06-23T06:25:00Z" }, { + "quantity" : 86.994338611676767, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T23:15:00Z", - "quantity" : 140.14904110452417 + "startDate" : "2023-06-23T06:30:00Z" }, { + "quantity" : 85.232136877351081, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T23:20:00Z", - "quantity" : 139.31472998543964 + "startDate" : "2023-06-23T06:35:00Z" }, { + "quantity" : 83.615651290380811, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T23:25:00Z", - "quantity" : 138.56421620595611 + "startDate" : "2023-06-23T06:40:00Z" }, { + "quantity" : 82.136746744082188, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T23:30:00Z", - "quantity" : 137.8932410364304 + "startDate" : "2023-06-23T06:45:00Z" }, { + "quantity" : 80.787935960558002, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T23:35:00Z", - "quantity" : 137.29767242360586 + "startDate" : "2023-06-23T06:50:00Z" }, { + "quantity" : 79.561150334091622, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T23:40:00Z", - "quantity" : 136.77350690107906 + "startDate" : "2023-06-23T06:55:00Z" }, { + "quantity" : 78.448809315519384, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T23:45:00Z", - "quantity" : 136.3168708208772 + "startDate" : "2023-06-23T07:00:00Z" }, { + "quantity" : 77.444295000376087, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T23:50:00Z", - "quantity" : 135.92402097664686 + "startDate" : "2023-06-23T07:05:00Z" }, { + "quantity" : 76.541144021775267, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-29T23:55:00Z", - "quantity" : 135.59134468329685 + "startDate" : "2023-06-23T07:10:00Z" }, { + "quantity" : 75.734033247701291, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-30T00:00:00Z", - "quantity" : 135.31535937266145 + "startDate" : "2023-06-23T07:15:00Z" }, { + "quantity" : 75.018229944400559, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-30T00:05:00Z", - "quantity" : 135.09271175987229 + "startDate" : "2023-06-23T07:20:00Z" }, { + "quantity" : 74.389076912965834, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-30T00:10:00Z", - "quantity" : 134.92017663057632 + "startDate" : "2023-06-23T07:25:00Z" }, { + "quantity" : 73.841309919727451, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-30T00:15:00Z", - "quantity" : 134.79465529494851 + "startDate" : "2023-06-23T07:30:00Z" }, { + "quantity" : 73.370549918316215, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-30T00:20:00Z", - "quantity" : 134.71317375053073 + "startDate" : "2023-06-23T07:35:00Z" }, { + "quantity" : 72.972744055408953, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-30T00:25:00Z", - "quantity" : 134.67288059232183 + "startDate" : "2023-06-23T07:40:00Z" }, { + "quantity" : 72.643975082565134, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-30T00:30:00Z", - "quantity" : 134.66535701098488 + "startDate" : "2023-06-23T07:45:00Z" }, { + "quantity" : 72.380461060355856, "quantityUnit" : "mg\/dL", - "startDate" : "2023-07-30T01:20:54Z", - "quantity" : 134.66535701098488 + "startDate" : "2023-06-23T07:50:00Z" + }, + { + "quantity" : 72.178520063294286, + "quantityUnit" : "mg\/dL", + "startDate" : "2023-06-23T07:55:00Z" + }, + { + "quantity" : 72.034174053629386, + "quantityUnit" : "mg\/dL", + "startDate" : "2023-06-23T08:00:00Z" + }, + { + "quantity" : 71.942299096190823, + "quantityUnit" : "mg\/dL", + "startDate" : "2023-06-23T08:05:00Z" + }, + { + "quantity" : 71.897751011456421, + "quantityUnit" : "mg\/dL", + "startDate" : "2023-06-23T08:10:00Z" + }, + { + "quantity" : 71.895123880236383, + "quantityUnit" : "mg\/dL", + "startDate" : "2023-06-23T08:15:00Z" + }, + { + "quantity" : 71.906254842464136, + "quantityUnit" : "mg\/dL", + "startDate" : "2023-06-23T08:20:00Z" + }, + { + "quantity" : 71.914434937142801, + "quantityUnit" : "mg\/dL", + "startDate" : "2023-06-23T08:25:00Z" + }, + { + "quantity" : 71.920167940771535, + "quantityUnit" : "mg\/dL", + "startDate" : "2023-06-23T08:30:00Z" + }, + { + "quantity" : 71.923927819981145, + "quantityUnit" : "mg\/dL", + "startDate" : "2023-06-23T08:35:00Z" + }, + { + "quantity" : 71.926159114246957, + "quantityUnit" : "mg\/dL", + "startDate" : "2023-06-23T08:40:00Z" + }, + { + "quantity" : 71.927280081079402, + "quantityUnit" : "mg\/dL", + "startDate" : "2023-06-23T08:45:00Z" + }, + { + "quantity" : 71.927682355083221, + "quantityUnit" : "mg\/dL", + "startDate" : "2023-06-23T08:50:00Z" + }, + { + "quantity" : 71.927731342958282, + "quantityUnit" : "mg\/dL", + "startDate" : "2023-06-23T08:55:00Z" + }, + { + "quantity" : 71.927731342958282, + "quantityUnit" : "mg\/dL", + "startDate" : "2023-06-23T09:00:00Z" } ] diff --git a/LoopTests/Managers/Alerts/AlertStoreTests.swift b/LoopTests/Managers/Alerts/AlertStoreTests.swift index 3f6286cf17..bb9d109633 100644 --- a/LoopTests/Managers/Alerts/AlertStoreTests.swift +++ b/LoopTests/Managers/Alerts/AlertStoreTests.swift @@ -72,8 +72,8 @@ class AlertStoreTests: XCTestCase { let object = StoredAlert(from: alert2, context: alertStore.managedObjectContext, issuedDate: Self.historicDate) XCTAssertNil(object.acknowledgedDate) XCTAssertNil(object.retractedDate) - XCTAssertEqual("{\"title\":\"title\",\"acknowledgeActionButtonLabel\":\"label\",\"body\":\"body\"}", object.backgroundContent) - XCTAssertEqual("{\"title\":\"title\",\"acknowledgeActionButtonLabel\":\"label\",\"body\":\"body\"}", object.foregroundContent) + XCTAssertEqual("{\"acknowledgeActionButtonLabel\":\"label\",\"body\":\"body\",\"title\":\"title\"}", object.backgroundContent) + XCTAssertEqual("{\"acknowledgeActionButtonLabel\":\"label\",\"body\":\"body\",\"title\":\"title\"}", object.foregroundContent) XCTAssertEqual("managerIdentifier2.alertIdentifier2", object.identifier.value) XCTAssertEqual(Self.historicDate, object.issuedDate) XCTAssertEqual(1, object.modificationCounter) @@ -870,14 +870,14 @@ class AlertStoreLogCriticalEventLogTests: XCTestCase { endDate: dateFormatter.date(from: "2100-01-02T03:09:00Z")!, to: outputStream, progress: progress)) - XCTAssertEqual(outputStream.string, """ -[ -{"acknowledgedDate":"2100-01-02T03:08:00.000Z","alertIdentifier":"a1","backgroundContent":"{\\\"title\\\":\\\"BACKGROUND\\\",\\\"acknowledgeActionButtonLabel\\\":\\\"OK\\\",\\\"body\\\":\\\"background\\\"}","interruptionLevel":"timeSensitive","issuedDate":"2100-01-02T03:08:00.000Z","managerIdentifier":"m1","modificationCounter":1,"syncIdentifier":"52A046F7-F449-49B2-B003-7A378D0002DE","triggerType":0}, -{"acknowledgedDate":"2100-01-02T03:04:00.000Z","alertIdentifier":"a3","backgroundContent":"{\\\"title\\\":\\\"BACKGROUND\\\",\\\"acknowledgeActionButtonLabel\\\":\\\"OK\\\",\\\"body\\\":\\\"background\\\"}","interruptionLevel":"timeSensitive","issuedDate":"2100-01-02T03:04:00.000Z","managerIdentifier":"m3","modificationCounter":3,"syncIdentifier":"285AEA4B-0DEE-41F4-8669-800E9582A6E7","triggerType":0}, -{"acknowledgedDate":"2100-01-02T03:06:00.000Z","alertIdentifier":"a4","backgroundContent":"{\\\"title\\\":\\\"BACKGROUND\\\",\\\"acknowledgeActionButtonLabel\\\":\\\"OK\\\",\\\"body\\\":\\\"background\\\"}","interruptionLevel":"timeSensitive","issuedDate":"2100-01-02T03:06:00.000Z","managerIdentifier":"m4","modificationCounter":4,"syncIdentifier":"4B3109BD-DE11-42BD-A777-D4783459C483","triggerType":0} -] -""" - ) + + XCTAssertEqual(outputStream.string, #""" + [ + {"acknowledgedDate":"2100-01-02T03:08:00.000Z","alertIdentifier":"a1","backgroundContent":"{\"acknowledgeActionButtonLabel\":\"OK\",\"body\":\"background\",\"title\":\"BACKGROUND\"}","interruptionLevel":"timeSensitive","issuedDate":"2100-01-02T03:08:00.000Z","managerIdentifier":"m1","modificationCounter":1,"syncIdentifier":"52A046F7-F449-49B2-B003-7A378D0002DE","triggerType":0}, + {"acknowledgedDate":"2100-01-02T03:04:00.000Z","alertIdentifier":"a3","backgroundContent":"{\"acknowledgeActionButtonLabel\":\"OK\",\"body\":\"background\",\"title\":\"BACKGROUND\"}","interruptionLevel":"timeSensitive","issuedDate":"2100-01-02T03:04:00.000Z","managerIdentifier":"m3","modificationCounter":3,"syncIdentifier":"285AEA4B-0DEE-41F4-8669-800E9582A6E7","triggerType":0}, + {"acknowledgedDate":"2100-01-02T03:06:00.000Z","alertIdentifier":"a4","backgroundContent":"{\"acknowledgeActionButtonLabel\":\"OK\",\"body\":\"background\",\"title\":\"BACKGROUND\"}","interruptionLevel":"timeSensitive","issuedDate":"2100-01-02T03:06:00.000Z","managerIdentifier":"m4","modificationCounter":4,"syncIdentifier":"4B3109BD-DE11-42BD-A777-D4783459C483","triggerType":0} + ] + """#) XCTAssertEqual(progress.completedUnitCount, 3 * 1) } diff --git a/LoopTests/Managers/Alerts/StoredAlertTests.swift b/LoopTests/Managers/Alerts/StoredAlertTests.swift index 504a672fae..85fe753c7d 100644 --- a/LoopTests/Managers/Alerts/StoredAlertTests.swift +++ b/LoopTests/Managers/Alerts/StoredAlertTests.swift @@ -45,10 +45,10 @@ class StoredAlertEncodableTests: XCTestCase { let storedAlert = StoredAlert(from: alert, context: managedObjectContext, syncIdentifier: UUID(uuidString: "A7073F28-0322-4506-A733-CF6E0687BAF7")!) XCTAssertEqual(.active, storedAlert.interruptionLevel) storedAlert.issuedDate = dateFormatter.date(from: "2020-05-14T21:00:12Z")! - try! assertStoredAlertEncodable(storedAlert, encodesJSON: """ + try! assertStoredAlertEncodable(storedAlert, encodesJSON: #""" { "alertIdentifier" : "bar", - "backgroundContent" : "{\\\"title\\\":\\\"BACKGROUND\\\",\\\"acknowledgeActionButtonLabel\\\":\\\"OK\\\",\\\"body\\\":\\\"background\\\"}", + "backgroundContent" : "{\"acknowledgeActionButtonLabel\":\"OK\",\"body\":\"background\",\"title\":\"BACKGROUND\"}", "interruptionLevel" : "active", "issuedDate" : "2020-05-14T21:00:12Z", "managerIdentifier" : "foo", @@ -56,15 +56,15 @@ class StoredAlertEncodableTests: XCTestCase { "syncIdentifier" : "A7073F28-0322-4506-A733-CF6E0687BAF7", "triggerType" : 0 } - """ + """# ) storedAlert.interruptionLevel = .critical XCTAssertEqual(.critical, storedAlert.interruptionLevel) - try! assertStoredAlertEncodable(storedAlert, encodesJSON: """ + try! assertStoredAlertEncodable(storedAlert, encodesJSON: #""" { "alertIdentifier" : "bar", - "backgroundContent" : "{\\\"title\\\":\\\"BACKGROUND\\\",\\\"acknowledgeActionButtonLabel\\\":\\\"OK\\\",\\\"body\\\":\\\"background\\\"}", + "backgroundContent" : "{\"acknowledgeActionButtonLabel\":\"OK\",\"body\":\"background\",\"title\":\"BACKGROUND\"}", "interruptionLevel" : "critical", "issuedDate" : "2020-05-14T21:00:12Z", "managerIdentifier" : "foo", @@ -72,7 +72,7 @@ class StoredAlertEncodableTests: XCTestCase { "syncIdentifier" : "A7073F28-0322-4506-A733-CF6E0687BAF7", "triggerType" : 0 } - """ + """# ) } } diff --git a/LoopTests/Managers/DoseEnactorTests.swift b/LoopTests/Managers/DoseEnactorTests.swift index 72359793e6..bf722ec874 100644 --- a/LoopTests/Managers/DoseEnactorTests.swift +++ b/LoopTests/Managers/DoseEnactorTests.swift @@ -121,7 +121,7 @@ class MockPumpManager: PumpManager { .minutes(units / deliveryUnitsPerMinute) } - var managerIdentifier: String = "MockPumpManager" + static var pluginIdentifier: String = "MockPumpManager" var localizedTitle: String = "MockPumpManager" diff --git a/LoopTests/Managers/LoopAlgorithmTests.swift b/LoopTests/Managers/LoopAlgorithmTests.swift new file mode 100644 index 0000000000..6c51283872 --- /dev/null +++ b/LoopTests/Managers/LoopAlgorithmTests.swift @@ -0,0 +1,75 @@ +// +// LoopAlgorithmTests.swift +// LoopTests +// +// Created by Pete Schwamb on 8/17/23. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import XCTest +import LoopKit +import LoopCore +import HealthKit + +final class LoopAlgorithmTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + public var bundle: Bundle { + return Bundle(for: type(of: self)) + } + + public func loadFixture(_ resourceName: String) -> T { + let path = bundle.path(forResource: resourceName, ofType: "json")! + return try! JSONSerialization.jsonObject(with: Data(contentsOf: URL(fileURLWithPath: path)), options: []) as! T + } + + func loadBasalRateScheduleFixture(_ resourceName: String) -> BasalRateSchedule { + let fixture: [JSONDictionary] = loadFixture(resourceName) + + let items = fixture.map { + return RepeatingScheduleValue(startTime: TimeInterval(minutes: $0["minutes"] as! Double), value: $0["rate"] as! Double) + } + + return BasalRateSchedule(dailyItems: items, timeZone: .utcTimeZone)! + } + + func loadPredictedGlucoseFixture(_ name: String) -> [PredictedGlucoseValue] { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + + let url = bundle.url(forResource: name, withExtension: "json")! + return try! decoder.decode([PredictedGlucoseValue].self, from: try! Data(contentsOf: url)) + } + + + func testLiveCaptureWithFunctionalAlgorithm() throws { + // This matches the "testForecastFromLiveCaptureInputData" test of LoopDataManagerDosingTests, + // Using the same input data, but generating the forecast using the LoopAlgorithm.generatePrediction() + // function. + + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + let url = bundle.url(forResource: "live_capture_input", withExtension: "json")! + let predictionInput = try! decoder.decode(LoopPredictionInput.self, from: try! Data(contentsOf: url)) + + let prediction = try LoopAlgorithm.generatePrediction(input: predictionInput) + + let expectedPredictedGlucose = loadPredictedGlucoseFixture("live_capture_predicted_glucose") + + XCTAssertEqual(expectedPredictedGlucose.count, prediction.glucose.count) + + let defaultAccuracy = 1.0 / 40.0 + + for (expected, calculated) in zip(expectedPredictedGlucose, prediction.glucose) { + XCTAssertEqual(expected.startDate, calculated.startDate) + XCTAssertEqual(expected.quantity.doubleValue(for: .milligramsPerDeciliter), calculated.quantity.doubleValue(for: .milligramsPerDeciliter), accuracy: defaultAccuracy) + } + } +} diff --git a/LoopTests/Managers/LoopDataManagerDosingTests.swift b/LoopTests/Managers/LoopDataManagerDosingTests.swift index 621aa348a5..a1f26a0e92 100644 --- a/LoopTests/Managers/LoopDataManagerDosingTests.swift +++ b/LoopTests/Managers/LoopDataManagerDosingTests.swift @@ -55,7 +55,83 @@ class LoopDataManagerDosingTests: LoopDataManagerTests { // MARK: Tests func testForecastFromLiveCaptureInputData() { - setUp(for: .liveCapture) + + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + let url = bundle.url(forResource: "live_capture_input", withExtension: "json")! + let predictionInput = try! decoder.decode(LoopPredictionInput.self, from: try! Data(contentsOf: url)) + + // Therapy settings in the "live capture" input only have one value, so we can fake some schedules + // from the first entry of each therapy setting's history. + let basalRateSchedule = BasalRateSchedule(dailyItems: [ + RepeatingScheduleValue(startTime: 0, value: predictionInput.settings.basal.first!.value) + ]) + let insulinSensitivitySchedule = InsulinSensitivitySchedule( + unit: .milligramsPerDeciliter, + dailyItems: [ + RepeatingScheduleValue(startTime: 0, value: predictionInput.settings.sensitivity.first!.value.doubleValue(for: .milligramsPerDeciliter)) + ], + timeZone: .utcTimeZone + )! + let carbRatioSchedule = CarbRatioSchedule( + unit: .gram(), + dailyItems: [ + RepeatingScheduleValue(startTime: 0.0, value: predictionInput.settings.carbRatio.first!.value) + ], + timeZone: .utcTimeZone + )! + + let settings = LoopSettings( + dosingEnabled: false, + glucoseTargetRangeSchedule: glucoseTargetRangeSchedule, + insulinSensitivitySchedule: insulinSensitivitySchedule, + basalRateSchedule: basalRateSchedule, + carbRatioSchedule: carbRatioSchedule, + maximumBasalRatePerHour: 10, + maximumBolus: 5, + suspendThreshold: predictionInput.settings.suspendThreshold, + automaticDosingStrategy: .automaticBolus + ) + + let glucoseStore = MockGlucoseStore() + glucoseStore.storedGlucose = predictionInput.glucoseHistory + + let currentDate = glucoseStore.latestGlucose!.startDate + now = currentDate + + let doseStore = MockDoseStore() + doseStore.basalProfile = basalRateSchedule + doseStore.basalProfileApplyingOverrideHistory = doseStore.basalProfile + doseStore.sensitivitySchedule = insulinSensitivitySchedule + doseStore.doseHistory = predictionInput.doses + doseStore.lastAddedPumpData = predictionInput.doses.last!.startDate + let carbStore = MockCarbStore() + carbStore.insulinSensitivityScheduleApplyingOverrideHistory = insulinSensitivitySchedule + carbStore.carbRatioSchedule = carbRatioSchedule + carbStore.carbRatioScheduleApplyingOverrideHistory = carbRatioSchedule + carbStore.carbHistory = predictionInput.carbEntries + + + dosingDecisionStore = MockDosingDecisionStore() + automaticDosingStatus = AutomaticDosingStatus(automaticDosingEnabled: true, isAutomaticDosingAllowed: true) + loopDataManager = LoopDataManager( + lastLoopCompleted: currentDate, + basalDeliveryState: .active(currentDate), + settings: settings, + overrideHistory: TemporaryScheduleOverrideHistory(), + analyticsServicesManager: AnalyticsServicesManager(), + localCacheDuration: .days(1), + doseStore: doseStore, + glucoseStore: glucoseStore, + carbStore: carbStore, + dosingDecisionStore: dosingDecisionStore, + latestStoredSettingsProvider: MockLatestStoredSettingsProvider(), + now: { currentDate }, + pumpInsulinType: .novolog, + automaticDosingStatus: automaticDosingStatus, + trustedTimeOffset: { 0 } + ) + let expectedPredictedGlucose = loadPredictedGlucoseFixture("live_capture_predicted_glucose") let updateGroup = DispatchGroup() @@ -63,7 +139,7 @@ class LoopDataManagerDosingTests: LoopDataManagerTests { var predictedGlucose: [PredictedGlucoseValue]? var recommendedBasal: TempBasalRecommendation? self.loopDataManager.getLoopState { _, state in - predictedGlucose = state.predictedGlucose + predictedGlucose = state.predictedGlucoseIncludingPendingInsulin recommendedBasal = state.recommendedAutomaticDose?.recommendation.basalAdjustment updateGroup.leave() } @@ -71,14 +147,13 @@ class LoopDataManagerDosingTests: LoopDataManagerTests { updateGroup.wait() XCTAssertNotNil(predictedGlucose) + XCTAssertEqual(expectedPredictedGlucose.count, predictedGlucose!.count) for (expected, calculated) in zip(expectedPredictedGlucose, predictedGlucose!) { XCTAssertEqual(expected.startDate, calculated.startDate) XCTAssertEqual(expected.quantity.doubleValue(for: .milligramsPerDeciliter), calculated.quantity.doubleValue(for: .milligramsPerDeciliter), accuracy: defaultAccuracy) } - - XCTAssertEqual(1.99, recommendedBasal!.unitsPerHour, accuracy: defaultAccuracy) } diff --git a/LoopTests/Managers/MealDetectionManagerTests.swift b/LoopTests/Managers/MealDetectionManagerTests.swift index 93713c727c..3db48cc7eb 100644 --- a/LoopTests/Managers/MealDetectionManagerTests.swift +++ b/LoopTests/Managers/MealDetectionManagerTests.swift @@ -12,6 +12,25 @@ import LoopCore import LoopKit @testable import Loop +fileprivate class MockGlucoseSample: GlucoseSampleValue { + + let provenanceIdentifier = "" + let isDisplayOnly: Bool + let wasUserEntered: Bool + let condition: LoopKit.GlucoseCondition? = nil + let trendRate: HKQuantity? = nil + var trend: LoopKit.GlucoseTrend? + var syncIdentifier: String? + let quantity: HKQuantity = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 100) + let startDate: Date + + init(startDate: Date, isDisplayOnly: Bool = false, wasUserEntered: Bool = false) { + self.startDate = startDate + self.isDisplayOnly = isDisplayOnly + self.wasUserEntered = wasUserEntered + } +} + enum MissedMealTestType { private static var dateFormatter = ISO8601DateFormatter.localTimeDate() @@ -175,6 +194,8 @@ class MealDetectionManagerTests: XCTestCase { var bolusUnits: Double? var bolusDurationEstimator: ((Double) -> TimeInterval?)! + fileprivate var glucoseSamples: [MockGlucoseSample]! + @discardableResult func setUp(for testType: MissedMealTestType) -> [GlucoseEffectVelocity] { carbStore = CarbStore( cacheStore: PersistenceController(directoryURL: URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).appendingPathComponent(UUID().uuidString, isDirectory: true)), @@ -209,6 +230,8 @@ class MealDetectionManagerTests: XCTestCase { test_currentDate: testType.currentDate ) + glucoseSamples = [MockGlucoseSample(startDate: now)] + bolusDurationEstimator = { units in self.bolusUnits = units return self.pumpManager.estimatedDuration(toBolus: units) @@ -263,7 +286,7 @@ class MealDetectionManagerTests: XCTestCase { let updateGroup = DispatchGroup() updateGroup.enter() - mealDetectionManager.hasMissedMeal(insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in + mealDetectionManager.hasMissedMeal(glucoseSamples: glucoseSamples, insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in XCTAssertEqual(status, .noMissedMeal) updateGroup.leave() } @@ -275,7 +298,7 @@ class MealDetectionManagerTests: XCTestCase { let updateGroup = DispatchGroup() updateGroup.enter() - mealDetectionManager.hasMissedMeal(insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in + mealDetectionManager.hasMissedMeal(glucoseSamples: glucoseSamples, insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in XCTAssertEqual(status, .noMissedMeal) updateGroup.leave() } @@ -288,7 +311,7 @@ class MealDetectionManagerTests: XCTestCase { let updateGroup = DispatchGroup() updateGroup.enter() - mealDetectionManager.hasMissedMeal(insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in + mealDetectionManager.hasMissedMeal(glucoseSamples: glucoseSamples, insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in XCTAssertEqual(status, .hasMissedMeal(startTime: testType.missedMealDate!, carbAmount: 55)) updateGroup.leave() } @@ -301,7 +324,7 @@ class MealDetectionManagerTests: XCTestCase { let updateGroup = DispatchGroup() updateGroup.enter() - mealDetectionManager.hasMissedMeal(insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in + mealDetectionManager.hasMissedMeal(glucoseSamples: glucoseSamples, insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in XCTAssertEqual(status, .hasMissedMeal(startTime: testType.missedMealDate!, carbAmount: 25)) updateGroup.leave() } @@ -314,7 +337,7 @@ class MealDetectionManagerTests: XCTestCase { let updateGroup = DispatchGroup() updateGroup.enter() - mealDetectionManager.hasMissedMeal(insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in + mealDetectionManager.hasMissedMeal(glucoseSamples: glucoseSamples, insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in XCTAssertEqual(status, .hasMissedMeal(startTime: testType.missedMealDate!, carbAmount: 50)) updateGroup.leave() } @@ -326,7 +349,7 @@ class MealDetectionManagerTests: XCTestCase { let updateGroup = DispatchGroup() updateGroup.enter() - mealDetectionManager.hasMissedMeal(insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in + mealDetectionManager.hasMissedMeal(glucoseSamples: glucoseSamples, insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in XCTAssertEqual(status, .noMissedMeal) updateGroup.leave() } @@ -339,7 +362,7 @@ class MealDetectionManagerTests: XCTestCase { let updateGroup = DispatchGroup() updateGroup.enter() - mealDetectionManager.hasMissedMeal(insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in + mealDetectionManager.hasMissedMeal(glucoseSamples: glucoseSamples, insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in XCTAssertEqual(status, .hasMissedMeal(startTime: testType.missedMealDate!, carbAmount: 40)) updateGroup.leave() } @@ -352,7 +375,7 @@ class MealDetectionManagerTests: XCTestCase { let updateGroup = DispatchGroup() updateGroup.enter() - mealDetectionManager.hasMissedMeal(insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in + mealDetectionManager.hasMissedMeal(glucoseSamples: glucoseSamples, insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in XCTAssertEqual(status, .hasMissedMeal(startTime: testType.missedMealDate!, carbAmount: 25)) updateGroup.leave() } @@ -460,6 +483,44 @@ class MealDetectionManagerTests: XCTestCase { XCTAssertEqual(bolusUnits, 4.5) XCTAssertEqual(mealDetectionManager.lastMissedMealNotification?.deliveryTime, expectedDeliveryTime2) } + + func testHasCalibrationPoints_NoNotification() { + let testType = MissedMealTestType.manyMeals + let counteractionEffects = setUp(for: testType) + + let calibratedGlucoseSamples = [MockGlucoseSample(startDate: now), MockGlucoseSample(startDate: now, isDisplayOnly: true)] + + let updateGroup = DispatchGroup() + updateGroup.enter() + mealDetectionManager.hasMissedMeal(glucoseSamples: calibratedGlucoseSamples, insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in + XCTAssertEqual(status, .noMissedMeal) + updateGroup.leave() + } + updateGroup.wait() + + let manualGlucoseSamples = [MockGlucoseSample(startDate: now), MockGlucoseSample(startDate: now, wasUserEntered: true)] + updateGroup.enter() + mealDetectionManager.hasMissedMeal(glucoseSamples: manualGlucoseSamples, insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in + XCTAssertEqual(status, .noMissedMeal) + updateGroup.leave() + } + updateGroup.wait() + } + + func testHasTooOldCalibrationPoint_NoImpactOnNotificationDelivery() { + let testType = MissedMealTestType.manyMeals + let counteractionEffects = setUp(for: testType) + + let tooOldCalibratedGlucoseSamples = [MockGlucoseSample(startDate: now, isDisplayOnly: false), MockGlucoseSample(startDate: now.addingTimeInterval(-MissedMealSettings.maxRecency-1), isDisplayOnly: true)] + + let updateGroup = DispatchGroup() + updateGroup.enter() + mealDetectionManager.hasMissedMeal(glucoseSamples: tooOldCalibratedGlucoseSamples, insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in + XCTAssertEqual(status, .hasMissedMeal(startTime: testType.missedMealDate!, carbAmount: 40)) + updateGroup.leave() + } + updateGroup.wait() + } } extension MealDetectionManagerTests { diff --git a/LoopTests/Managers/SupportManagerTests.swift b/LoopTests/Managers/SupportManagerTests.swift index f245549573..48fa42e4d8 100644 --- a/LoopTests/Managers/SupportManagerTests.swift +++ b/LoopTests/Managers/SupportManagerTests.swift @@ -16,9 +16,9 @@ class SupportManagerTests: XCTestCase { enum MockError: Error { case nothing } class Mixin { - func supportMenuItem(supportInfoProvider: SupportInfoProvider, urlHandler: @escaping (URL) -> Void) -> AnyView? { - nil - } + @ViewBuilder + func supportMenuItem(supportInfoProvider: SupportInfoProvider, urlHandler: @escaping (URL) -> Void) -> some View {} + func softwareUpdateView(bundleIdentifier: String, currentVersion: String, guidanceColors: GuidanceColors, openAppStore: (() -> Void)?) -> AnyView? { nil } @@ -34,7 +34,7 @@ class SupportManagerTests: XCTestCase { weak var delegate: SupportUIDelegate? } class MockSupport: Mixin, SupportUI { - static var supportIdentifier: String { "SupportManagerTestsMockSupport" } + static var pluginIdentifier: String { "SupportManagerTestsMockSupport" } override init() { super.init() } required init?(rawState: RawStateValue) { super.init() } var rawState: RawStateValue = [:] @@ -42,12 +42,11 @@ class SupportManagerTests: XCTestCase { func getScenarios(from scenarioURLs: [URL]) -> [LoopScenario] { [] } func loopWillReset() {} func loopDidReset() {} - func initializationComplete(for services: [LoopKit.Service]) {} func configurationMenuItems() -> [LoopKitUI.CustomMenuItem] { return [] } } class AnotherMockSupport: Mixin, SupportUI { - static var supportIdentifier: String { "SupportManagerTestsAnotherMockSupport" } + static var pluginIdentifier: String { "SupportManagerTestsAnotherMockSupport" } override init() { super.init() } required init?(rawState: RawStateValue) { super.init() } var rawState: RawStateValue = [:] @@ -55,7 +54,6 @@ class SupportManagerTests: XCTestCase { func getScenarios(from scenarioURLs: [URL]) -> [LoopScenario] { [] } func loopWillReset() {} func loopDidReset() {} - func initializationComplete(for services: [LoopKit.Service]) {} func configurationMenuItems() -> [LoopKitUI.CustomMenuItem] { return [] } } diff --git a/LoopTests/Mock Stores/MockCarbStore.swift b/LoopTests/Mock Stores/MockCarbStore.swift index 25537d2267..4a5c016eb5 100644 --- a/LoopTests/Mock Stores/MockCarbStore.swift +++ b/LoopTests/Mock Stores/MockCarbStore.swift @@ -108,8 +108,8 @@ class MockCarbStore: CarbStoreProtocol { let carbDates = samples.map { $0.startDate } let maxCarbDate = carbDates.max()! let minCarbDate = carbDates.min()! - let carbRatio = carbRatioScheduleApplyingOverrideHistory.between(start: maxCarbDate, end: minCarbDate) - let insulinSensitivity = insulinSensitivityScheduleApplyingOverrideHistory.quantitiesBetween(start: maxCarbDate, end: minCarbDate) + let carbRatio = carbRatioScheduleApplyingOverrideHistory.between(start: minCarbDate, end: maxCarbDate) + let insulinSensitivity = insulinSensitivityScheduleApplyingOverrideHistory.quantitiesBetween(start: minCarbDate, end: maxCarbDate) let effects = samples.map( to: effectVelocities, carbRatio: carbRatio, @@ -174,5 +174,4 @@ extension MockCarbStore { return nil } } - } diff --git a/LoopTests/Mock Stores/MockDoseStore.swift b/LoopTests/Mock Stores/MockDoseStore.swift index a676fa9e74..207596f31b 100644 --- a/LoopTests/Mock Stores/MockDoseStore.swift +++ b/LoopTests/Mock Stores/MockDoseStore.swift @@ -11,7 +11,6 @@ import LoopKit @testable import Loop class MockDoseStore: DoseStoreProtocol { - var doseHistory: [DoseEntry]? var sensitivitySchedule: InsulinSensitivitySchedule? @@ -41,7 +40,7 @@ class MockDoseStore: DoseStoreProtocol { // Default to the adult exponential insulin model var insulinModelProvider: InsulinModelProvider = StaticInsulinModelProvider(ExponentialInsulinModelPreset.rapidActingAdult) - var longestEffectDuration: TimeInterval = ExponentialInsulinModelPreset.rapidActingAdult.actionDuration + var longestEffectDuration: TimeInterval = ExponentialInsulinModelPreset.rapidActingAdult.effectDuration var insulinSensitivitySchedule: InsulinSensitivitySchedule? @@ -55,7 +54,7 @@ class MockDoseStore: DoseStoreProtocol { var lastAddedPumpData: Date - func addPumpEvents(_ events: [NewPumpEvent], lastReconciliation: Date?, completion: @escaping (DoseStore.DoseStoreError?) -> Void) { + func addPumpEvents(_ events: [NewPumpEvent], lastReconciliation: Date?, replacePendingEvents: Bool, completion: @escaping (DoseStore.DoseStoreError?) -> Void) { completion(nil) } @@ -92,7 +91,7 @@ class MockDoseStore: DoseStoreProtocol { } func getGlucoseEffects(start: Date, end: Date? = nil, basalDosingEnd: Date? = Date(), completion: @escaping (_ result: DoseStoreResult<[GlucoseEffect]>) -> Void) { - if let doseHistory, let sensitivitySchedule { + if let doseHistory, let sensitivitySchedule, let basalProfile = basalProfileApplyingOverrideHistory { // To properly know glucose effects at startDate, we need to go back another DIA hours let doseStart = start.addingTimeInterval(-longestEffectDuration) let doses = doseHistory.filterDateRange(doseStart, end) @@ -103,7 +102,9 @@ class MockDoseStore: DoseStoreProtocol { return dose.trimmed(to: basalDosingEnd) } - let glucoseEffects = trimmedDoses.glucoseEffects(insulinModelProvider: self.insulinModelProvider, longestEffectDuration: self.longestEffectDuration, insulinSensitivity: sensitivitySchedule, from: start, to: end) + let annotatedDoses = trimmedDoses.annotated(with: basalProfile) + + let glucoseEffects = annotatedDoses.glucoseEffects(insulinModelProvider: self.insulinModelProvider, longestEffectDuration: self.longestEffectDuration, insulinSensitivity: sensitivitySchedule, from: start, to: end) completion(.success(glucoseEffects.filterDateRange(start, end))) } else { return completion(.success(getCannedGlucoseEffects())) diff --git a/LoopTests/Mock Stores/MockGlucoseStore.swift b/LoopTests/Mock Stores/MockGlucoseStore.swift index 93b96f176b..19a6bc22e8 100644 --- a/LoopTests/Mock Stores/MockGlucoseStore.swift +++ b/LoopTests/Mock Stores/MockGlucoseStore.swift @@ -12,7 +12,7 @@ import LoopKit class MockGlucoseStore: GlucoseStoreProtocol { - init(for scenario: DosingTestScenario) { + init(for scenario: DosingTestScenario = .flatAndStable) { self.scenario = scenario // The store returns different effect values based on the scenario storedGlucose = loadHistoricGlucose(scenario: scenario) } @@ -80,12 +80,12 @@ class MockGlucoseStore: GlucoseStoreProtocol { } func counteractionEffects(for samples: [Sample], to effects: [GlucoseEffect]) -> [GlucoseEffectVelocity] where Sample : GlucoseSampleValue { - return [] // TODO: check if we'll ever want to test this + samples.counteractionEffects(to: effects) } - func getRecentMomentumEffect(_ completion: @escaping (_ effects: Result<[GlucoseEffect], Error>) -> Void) { + func getRecentMomentumEffect(for date: Date? = nil, _ completion: @escaping (_ effects: Result<[GlucoseEffect], Error>) -> Void) { if let storedGlucose { - let samples = storedGlucose.filterDateRange(scenario.currentDate.addingTimeInterval(-GlucoseMath.momentumDataInterval), nil) + let samples = storedGlucose.filterDateRange((date ?? Date()).addingTimeInterval(-GlucoseMath.momentumDataInterval), nil) completion(.success(samples.linearMomentumEffect())) } else { let fixture: [JSONDictionary] = loadFixture(momentumEffectToLoad) diff --git a/LoopTests/ViewModels/BolusEntryViewModelTests.swift b/LoopTests/ViewModels/BolusEntryViewModelTests.swift index 787deb1834..7f2c421ebf 100644 --- a/LoopTests/ViewModels/BolusEntryViewModelTests.swift +++ b/LoopTests/ViewModels/BolusEntryViewModelTests.swift @@ -58,9 +58,31 @@ class BolusEntryViewModelTests: XCTestCase { fileprivate var delegate: MockBolusEntryViewModelDelegate! var now: Date = BolusEntryViewModelTests.now - let mockOriginalCarbEntry = StoredCarbEntry(uuid: UUID(), provenanceIdentifier: "provenanceIdentifier", syncIdentifier: "syncIdentifier", syncVersion: 0, startDate: BolusEntryViewModelTests.exampleStartDate, quantity: BolusEntryViewModelTests.exampleCarbQuantity, foodType: "foodType", absorptionTime: 1, createdByCurrentApp: true, userCreatedDate: BolusEntryViewModelTests.now, userUpdatedDate: BolusEntryViewModelTests.now) + let mockOriginalCarbEntry = StoredCarbEntry( + startDate: BolusEntryViewModelTests.exampleStartDate, + quantity: BolusEntryViewModelTests.exampleCarbQuantity, + uuid: UUID(), + provenanceIdentifier: "provenanceIdentifier", + syncIdentifier: "syncIdentifier", + syncVersion: 0, + foodType: "foodType", + absorptionTime: 1, + createdByCurrentApp: true, + userCreatedDate: BolusEntryViewModelTests.now, + userUpdatedDate: BolusEntryViewModelTests.now) let mockPotentialCarbEntry = NewCarbEntry(quantity: BolusEntryViewModelTests.exampleCarbQuantity, startDate: BolusEntryViewModelTests.exampleStartDate, foodType: "foodType", absorptionTime: 1) - let mockFinalCarbEntry = StoredCarbEntry(uuid: UUID(), provenanceIdentifier: "provenanceIdentifier", syncIdentifier: "syncIdentifier", syncVersion: 1, startDate: BolusEntryViewModelTests.exampleStartDate, quantity: BolusEntryViewModelTests.exampleCarbQuantity, foodType: "foodType", absorptionTime: 1, createdByCurrentApp: true, userCreatedDate: BolusEntryViewModelTests.now, userUpdatedDate: BolusEntryViewModelTests.now) + let mockFinalCarbEntry = StoredCarbEntry( + startDate: BolusEntryViewModelTests.exampleStartDate, + quantity: BolusEntryViewModelTests.exampleCarbQuantity, + uuid: UUID(), + provenanceIdentifier: "provenanceIdentifier", + syncIdentifier: "syncIdentifier", + syncVersion: 1, + foodType: "foodType", + absorptionTime: 1, + createdByCurrentApp: true, + userCreatedDate: BolusEntryViewModelTests.now, + userUpdatedDate: BolusEntryViewModelTests.now) let mockUUID = BolusEntryViewModelTests.mockUUID.uuidString let queue = DispatchQueue(label: "BolusEntryViewModelTests") var saveAndDeliverSuccess = false @@ -261,7 +283,7 @@ class BolusEntryViewModelTests: XCTestCase { } func testUpdateCarbsOnBoard() async throws { - delegate.carbsOnBoardResult = .success(CarbValue(startDate: Self.exampleStartDate, endDate: Self.exampleEndDate, quantity: Self.exampleCarbQuantity)) + delegate.carbsOnBoardResult = .success(CarbValue(startDate: Self.exampleStartDate, endDate: Self.exampleEndDate, value: Self.exampleCarbQuantity.doubleValue(for: .gram()))) XCTAssertNil(bolusEntryViewModel.activeCarbs) await bolusEntryViewModel.update() XCTAssertEqual(Self.exampleCarbQuantity, bolusEntryViewModel.activeCarbs) @@ -680,14 +702,14 @@ class BolusEntryViewModelTests: XCTestCase { func testCarbEntryDateAndAbsorptionTimeString() async throws { await setUpViewModel(originalCarbEntry: mockOriginalCarbEntry, potentialCarbEntry: mockPotentialCarbEntry) - XCTAssertEqual("12:00 PM + 0m", bolusEntryViewModel.carbEntryDateAndAbsorptionTimeString) + XCTAssertEqual("12:00 PM + 0m", bolusEntryViewModel.carbEntryDateAndAbsorptionTimeString) } func testCarbEntryDateAndAbsorptionTimeString2() async throws { let potentialCarbEntry = NewCarbEntry(quantity: BolusEntryViewModelTests.exampleCarbQuantity, startDate: Self.exampleStartDate, foodType: nil, absorptionTime: nil) await setUpViewModel(originalCarbEntry: mockOriginalCarbEntry, potentialCarbEntry: potentialCarbEntry) - XCTAssertEqual("12:00 PM", bolusEntryViewModel.carbEntryDateAndAbsorptionTimeString) + XCTAssertEqual("12:00 PM", bolusEntryViewModel.carbEntryDateAndAbsorptionTimeString) } func testIsManualGlucosePromptVisible() throws { diff --git a/LoopTests/ViewModels/SimpleBolusViewModelTests.swift b/LoopTests/ViewModels/SimpleBolusViewModelTests.swift index 7aab8112f4..94c1fd8661 100644 --- a/LoopTests/ViewModels/SimpleBolusViewModelTests.swift +++ b/LoopTests/ViewModels/SimpleBolusViewModelTests.swift @@ -294,12 +294,12 @@ extension SimpleBolusViewModelTests: SimpleBolusViewModelDelegate { addedCarbEntry = carbEntry let storedCarbEntry = StoredCarbEntry( + startDate: carbEntry.startDate, + quantity: carbEntry.quantity, uuid: UUID(), provenanceIdentifier: UUID().uuidString, syncIdentifier: UUID().uuidString, syncVersion: 1, - startDate: carbEntry.startDate, - quantity: carbEntry.quantity, foodType: carbEntry.foodType, absorptionTime: carbEntry.absorptionTime, createdByCurrentApp: true, diff --git a/LoopUI/Extensions/DismissibleHostingController.swift b/LoopUI/Extensions/DismissibleHostingController.swift index ce0bab23b9..47670f1b27 100644 --- a/LoopUI/Extensions/DismissibleHostingController.swift +++ b/LoopUI/Extensions/DismissibleHostingController.swift @@ -10,13 +10,13 @@ import SwiftUI import LoopKitUI extension DismissibleHostingController { - public convenience init( + public convenience init( rootView: Content, dismissalMode: DismissalMode = .modalDismiss, isModalInPresentation: Bool = true, onDisappear: @escaping () -> Void = {} ) { - self.init(rootView: rootView, + self.init(content: rootView, dismissalMode: dismissalMode, isModalInPresentation: isModalInPresentation, onDisappear: onDisappear, diff --git a/LoopUI/nb.lproj/Localizable.strings b/LoopUI/nb.lproj/Localizable.strings index cf8aa03166..eb5ce50cb9 100644 --- a/LoopUI/nb.lproj/Localizable.strings +++ b/LoopUI/nb.lproj/Localizable.strings @@ -1,14 +1,14 @@ /* Green closed loop ON message (1: last loop string) (2: app name) */ -"\n%1$@\n\n%2$@ is operating with Closed Loop in the ON position." = "\n%1$@\n\n%2$@ opererer med Closed Loop i ON-posisjon."; +"\n%1$@\n\n%2$@ is operating with Closed Loop in the ON position." = "%1$@\n\n%2$@ opererer med Closed Loop i ON-posisjon."; /* Red loop message (1: last loop string) (2: app name) */ -"\n%1$@\n\nTap your CGM and insulin pump status icons for more information. %2$@ will continue trying to complete a loop, but check for potential communication issues with your pump and CGM." = "\n%1$@\n\n Trykk på statusikonene for CGM og insulinpumpe for mer informasjon. %2$@ vil fortsette å prøve å fullføre en sløyfe, men se etter potensielle kommunikasjonsproblemer med pumpen og CGM."; +"\n%1$@\n\nTap your CGM and insulin pump status icons for more information. %2$@ will continue trying to complete a loop, but check for potential communication issues with your pump and CGM." = "%1$@\n\n Trykk på statusikonene for CGM og insulinpumpe for mer informasjon. %2$@ vil fortsette å prøve å fullføre en sløyfe, men se etter potensielle kommunikasjonsproblemer med pumpen og CGM."; /* Yellow loop message (1: last loop string) (2: app name) */ -"\n%1$@\n\nTap your CGM and insulin pump status icons for more information. %2$@ will continue trying to complete a loop, but watch for potential communication issues with your pump and CGM." = "\n%1$@\n\nTrykk på statusikonene for CGM og insulinpumpe for mer informasjon. %2$@ vil fortsette å prøve å fullføre en loop, men se etter potensielle kommunikasjonsproblemer med pumpen og CGM."; +"\n%1$@\n\nTap your CGM and insulin pump status icons for more information. %2$@ will continue trying to complete a loop, but watch for potential communication issues with your pump and CGM." = "%1$@\n\nTrykk på statusikonene for CGM og insulinpumpe for mer informasjon. %2$@ vil fortsette å prøve å fullføre en loop, men se etter potensielle kommunikasjonsproblemer med pumpen og CGM."; /* Green closed loop OFF message (1: app name)(2: reason for open loop) */ -"\n%1$@ is operating with Closed Loop in the OFF position. Your pump and CGM will continue operating, but the app will not adjust dosing automatically.\n\n%2$@" = "\n%1$@ opererer med Closed Loop i OFF posisjon. Pumpen og CGM vil fortsette å fungere, men appen vil ikke justere doseringen automatisk.\n\n%2$@"; +"\n%1$@ is operating with Closed Loop in the OFF position. Your pump and CGM will continue operating, but the app will not adjust dosing automatically.\n\n%2$@" = "%1$@ opererer med Closed Loop i OFF posisjon. Pumpen og CGM vil fortsette å fungere, men appen vil ikke justere doseringen automatisk.\n\n%2$@"; /* No glucose value representation (3 dashes for mg/dL) */ "– – –" = "– – –"; diff --git a/Scripts/capture-build-details.sh b/Scripts/capture-build-details.sh index 66f827d7c3..6122592374 100755 --- a/Scripts/capture-build-details.sh +++ b/Scripts/capture-build-details.sh @@ -10,10 +10,9 @@ SCRIPT_DIRECTORY="$(dirname "${0}")" error() { echo "ERROR: ${*}" >&2 - echo "Usage: ${SCRIPT} [-r|--git-source-root git-source-root] [-p|--provisioning-profile-path provisioning-profile-path] [-i|--info-plist-path info-plist-path]" >&2 + echo "Usage: ${SCRIPT} [-r|--git-source-root git-source-root] [-p|--provisioning-profile-path provisioning-profile-path]" >&2 echo "Parameters:" >&2 echo " -p|--provisioning-profile-path path to the .mobileprovision provisioning profile file to check for expiration; optional, defaults to \${HOME}/Library/MobileDevice/Provisioning Profiles/\${EXPANDED_PROVISIONING_PROFILE}.mobileprovision" >&2 - echo " -i|--info-plist-path path to the Info.plist file to modify; optional, defaults to \${BUILT_PRODUCTS_DIR}/\${INFOPLIST_PATH}" >&2 exit 1 } diff --git a/Version.xcconfig b/Version.xcconfig index 373efdca05..a7c7fe29d1 100644 --- a/Version.xcconfig +++ b/Version.xcconfig @@ -7,7 +7,7 @@ // // Version [DEFAULT] -LOOP_MARKETING_VERSION = 3.3.0 +LOOP_MARKETING_VERSION = 3.5.0 CURRENT_PROJECT_VERSION = 57 // Optional override diff --git a/WatchApp Extension/it.lproj/Localizable.strings b/WatchApp Extension/it.lproj/Localizable.strings index 49989cb3fa..2ec6d8bdc3 100644 --- a/WatchApp Extension/it.lproj/Localizable.strings +++ b/WatchApp Extension/it.lproj/Localizable.strings @@ -11,7 +11,7 @@ "%1$@ – %2$@ %3$@" = "%1$@ – %2$@ %3$@"; /* HUD row title for COB */ -"Active Carbs" = "Carb Attivi"; +"Active Carbs" = "Carboidrati Attivi"; /* HUD row title for IOB */ "Active Insulin" = "Insulina attiva"; @@ -118,5 +118,5 @@ "Unable to Reach iPhone" = "Impossibile raggiungere iPhone"; /* The text for the Watch button for enabling workout mode */ -"Workout" = "Allenarsi"; +"Workout" = "Allenamento"; diff --git a/WatchApp Extension/pl.lproj/Localizable.strings b/WatchApp Extension/pl.lproj/Localizable.strings index c8d0ba1465..a11ca5bd91 100644 --- a/WatchApp Extension/pl.lproj/Localizable.strings +++ b/WatchApp Extension/pl.lproj/Localizable.strings @@ -69,7 +69,7 @@ "On" = "Włącz"; /* The text for the Watch button for enabling a temporary override */ -"Override" = "Pominięcie"; +"Override" = "Cel Tymczasowy"; /* Alert message for updated bolus recommendation on Apple Watch */ "Please reconfirm the bolus amount." = "Potwierdź ponownie wielkość bolusa."; diff --git a/WatchApp/pl.lproj/Interface.strings b/WatchApp/pl.lproj/Interface.strings index 6199faad1e..341bbf4aeb 100644 --- a/WatchApp/pl.lproj/Interface.strings +++ b/WatchApp/pl.lproj/Interface.strings @@ -29,7 +29,7 @@ "MZU-QV-PtZ.text" = "TYTUŁ"; /* Class = "WKInterfaceLabel"; text = "Override"; ObjectID = "nC0-X3-oFJ"; */ -"nC0-X3-oFJ.text" = "Pominięcie"; +"nC0-X3-oFJ.text" = "Cel Tymczasowy"; /* Class = "WKInterfaceController"; title = "Loop"; ObjectID = "rNf-Mh-tID"; */ "rNf-Mh-tID.title" = "Loop";