Sfoglia il codice sorgente

Merge branch 'core-data-sync-trio' of github.com:dnzxy/Trio-dev into adjustments

Deniz Cengiz 1 anno fa
parent
commit
b4b3ea8382
63 ha cambiato i file con 634 aggiunte e 901 eliminazioni
  1. 4 12
      FreeAPS.xcodeproj/project.pbxproj
  2. 0 1
      FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json
  3. 0 97
      FreeAPS/Sources/APS/APSManager.swift
  4. 0 52
      FreeAPS/Sources/APS/FetchAnnouncementsManager.swift
  5. 0 2
      FreeAPS/Sources/APS/OpenAPS/Constants.swift
  6. 0 80
      FreeAPS/Sources/APS/Storage/AnnouncementsStorage.swift
  7. 1 1
      FreeAPS/Sources/APS/Storage/OverrideStorage.swift
  8. 0 1
      FreeAPS/Sources/Application/FreeAPSApp.swift
  9. 0 1
      FreeAPS/Sources/Assemblies/APSAssembly.swift
  10. 0 1
      FreeAPS/Sources/Assemblies/StorageAssembly.swift
  11. 0 6
      FreeAPS/Sources/Localizations/Main/ar.lproj/Localizable.strings
  12. 0 6
      FreeAPS/Sources/Localizations/Main/ca.lproj/Localizable.strings
  13. 0 6
      FreeAPS/Sources/Localizations/Main/da.lproj/Localizable.strings
  14. 0 6
      FreeAPS/Sources/Localizations/Main/de.lproj/Localizable.strings
  15. 0 6
      FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings
  16. 0 6
      FreeAPS/Sources/Localizations/Main/es.lproj/Localizable.strings
  17. 0 6
      FreeAPS/Sources/Localizations/Main/fi.lproj/Localizable.strings
  18. 0 6
      FreeAPS/Sources/Localizations/Main/fr.lproj/Localizable.strings
  19. 0 6
      FreeAPS/Sources/Localizations/Main/he.lproj/Localizable.strings
  20. 0 6
      FreeAPS/Sources/Localizations/Main/hu.lproj/Localizable.strings
  21. 0 6
      FreeAPS/Sources/Localizations/Main/it.lproj/Localizable.strings
  22. 0 6
      FreeAPS/Sources/Localizations/Main/nb.lproj/Localizable.strings
  23. 0 6
      FreeAPS/Sources/Localizations/Main/nl.lproj/Localizable.strings
  24. 0 6
      FreeAPS/Sources/Localizations/Main/pl.lproj/Localizable.strings
  25. 0 6
      FreeAPS/Sources/Localizations/Main/pt-BR.lproj/Localizable.strings
  26. 0 6
      FreeAPS/Sources/Localizations/Main/pt-PT.lproj/Localizable.strings
  27. 0 6
      FreeAPS/Sources/Localizations/Main/ru.lproj/Localizable.strings
  28. 0 6
      FreeAPS/Sources/Localizations/Main/sk.lproj/Localizable.strings
  29. 0 6
      FreeAPS/Sources/Localizations/Main/sv.lproj/Localizable.strings
  30. 0 6
      FreeAPS/Sources/Localizations/Main/tr.lproj/Localizable.strings
  31. 0 6
      FreeAPS/Sources/Localizations/Main/uk.lproj/Localizable.strings
  32. 0 6
      FreeAPS/Sources/Localizations/Main/vi.lproj/Localizable.strings
  33. 0 6
      FreeAPS/Sources/Localizations/Main/zh-Hans.lproj/Localizable.strings
  34. 0 57
      FreeAPS/Sources/Models/Announcement.swift
  35. 0 5
      FreeAPS/Sources/Models/FreeAPSSettings.swift
  36. 3 1
      FreeAPS/Sources/Modules/AutosensSettings/AutosensSettingsDataFlow.swift
  37. 7 1
      FreeAPS/Sources/Modules/AutosensSettings/AutosensSettingsProvider.swift
  38. 42 0
      FreeAPS/Sources/Modules/AutosensSettings/AutosensSettingsStateModel.swift
  39. 108 0
      FreeAPS/Sources/Modules/AutosensSettings/View/AutosensSettingsRootView.swift
  40. 0 1
      FreeAPS/Sources/Modules/Home/HomeDataFlow.swift
  41. 0 7
      FreeAPS/Sources/Modules/Home/HomeProvider.swift
  42. 0 2
      FreeAPS/Sources/Modules/Home/HomeStateModel.swift
  43. 46 25
      FreeAPS/Sources/Modules/Home/View/Chart/ChartElements/BasalChart.swift
  44. 1 1
      FreeAPS/Sources/Modules/Home/View/Chart/ChartElements/OverrideView.swift
  45. 229 0
      FreeAPS/Sources/Modules/Home/View/Chart/ChartLegendView.swift
  46. 62 144
      FreeAPS/Sources/Modules/Home/View/HomeRootView.swift
  47. 0 1
      FreeAPS/Sources/Modules/ISFEditor/ISFEditorDataFlow.swift
  48. 0 6
      FreeAPS/Sources/Modules/ISFEditor/ISFEditorProvider.swift
  49. 0 34
      FreeAPS/Sources/Modules/ISFEditor/ISFEditorStateModel.swift
  50. 0 45
      FreeAPS/Sources/Modules/ISFEditor/View/ISFEditorRootView.swift
  51. 0 2
      FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift
  52. 1 1
      FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutConfigRootView.swift
  53. 2 35
      FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutFetchView.swift
  54. 0 4
      FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift
  55. 24 0
      FreeAPS/Sources/Modules/Treatments/TreatmentsStateModel.swift
  56. 31 13
      FreeAPS/Sources/Modules/Treatments/View/TreatmentsRootView.swift
  57. 0 35
      FreeAPS/Sources/Services/Network/Nightscout/NightscoutAPI.swift
  58. 42 72
      FreeAPS/Sources/Services/Network/Nightscout/NightscoutManager.swift
  59. 26 1
      FreeAPS/Sources/Views/DefinitionRow.swift
  60. 2 18
      FreeAPS/Sources/Views/SettingInputHintView.swift
  61. 1 1
      FreeAPS/Sources/Views/TagCloudView.swift
  62. 0 1
      LiveActivity/Views/WidgetItems/LiveActivityIOBLabelView.swift
  63. 2 2
      Trio.xcworkspace/xcshareddata/swiftpm/Package.resolved

+ 4 - 12
FreeAPS.xcodeproj/project.pbxproj

@@ -121,8 +121,6 @@
 		38569349270B5DFB0002C50D /* AppGroupSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38569346270B5DFB0002C50D /* AppGroupSource.swift */; };
 		38569353270B5E350002C50D /* CGMRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38569352270B5E350002C50D /* CGMRootView.swift */; };
 		385CEA8225F23DFD002D6D5B /* NightscoutStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385CEA8125F23DFD002D6D5B /* NightscoutStatus.swift */; };
-		385CEAC125F2EA52002D6D5B /* Announcement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385CEAC025F2EA52002D6D5B /* Announcement.swift */; };
-		385CEAC425F2F154002D6D5B /* AnnouncementsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385CEAC325F2F154002D6D5B /* AnnouncementsStorage.swift */; };
 		3862CC2E2743F9F700BF832C /* CalendarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3862CC2D2743F9F700BF832C /* CalendarManager.swift */; };
 		3870FF4725EC187A0088248F /* BloodGlucose.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3870FF4225EC13F40088248F /* BloodGlucose.swift */; };
 		3871F39C25ED892B0013ECB5 /* TempTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3871F39B25ED892B0013ECB5 /* TempTarget.swift */; };
@@ -148,7 +146,6 @@
 		38A0363B25ECF07E00FCBB52 /* GlucoseStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A0363A25ECF07E00FCBB52 /* GlucoseStorage.swift */; };
 		38A0364225ED069400FCBB52 /* TempBasal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A0364125ED069400FCBB52 /* TempBasal.swift */; };
 		38A13D3225E28B4B00EAA382 /* PumpHistoryEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A13D3125E28B4B00EAA382 /* PumpHistoryEvent.swift */; };
-		38A43598262E0E4900E80935 /* FetchAnnouncementsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A43597262E0E4900E80935 /* FetchAnnouncementsManager.swift */; };
 		38A504A425DD9C4000C5B9E8 /* UserDefaultsExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A5049125DD9C4000C5B9E8 /* UserDefaultsExtensions.swift */; };
 		38A9260525F012D8009E3739 /* CarbRatios.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A9260425F012D8009E3739 /* CarbRatios.swift */; };
 		38AAF85525FFF846004AF583 /* CurrentGlucoseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AAF85425FFF846004AF583 /* CurrentGlucoseView.swift */; };
@@ -488,6 +485,7 @@
 		DD9ECB742CA9A0C300AA7C45 /* RemoteControlConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB732CA9A0C300AA7C45 /* RemoteControlConfig.swift */; };
 		DDA6E3202D258E0500C2988C /* OverrideHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6E31F2D258E0500C2988C /* OverrideHelpView.swift */; };
 		DDA6E3222D25901100C2988C /* TempTargetHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6E3212D25901100C2988C /* TempTargetHelpView.swift */; };
+		DDA6E2502D22187500C2988C /* ChartLegendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6E24F2D22187500C2988C /* ChartLegendView.swift */; };
 		DDB37CC52D05048F00D99BF4 /* ContactImageStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB37CC42D05048F00D99BF4 /* ContactImageStorage.swift */; };
 		DDB37CC72D05127500D99BF4 /* FontExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB37CC62D05127500D99BF4 /* FontExtensions.swift */; };
 		DDCEBF5B2CC1B76400DF4C36 /* LiveActivity+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCEBF5A2CC1B76400DF4C36 /* LiveActivity+Helper.swift */; };
@@ -819,8 +817,6 @@
 		38569346270B5DFB0002C50D /* AppGroupSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppGroupSource.swift; sourceTree = "<group>"; };
 		38569352270B5E350002C50D /* CGMRootView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGMRootView.swift; sourceTree = "<group>"; };
 		385CEA8125F23DFD002D6D5B /* NightscoutStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutStatus.swift; sourceTree = "<group>"; };
-		385CEAC025F2EA52002D6D5B /* Announcement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Announcement.swift; sourceTree = "<group>"; };
-		385CEAC325F2F154002D6D5B /* AnnouncementsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnouncementsStorage.swift; sourceTree = "<group>"; };
 		3862CC2D2743F9F700BF832C /* CalendarManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarManager.swift; sourceTree = "<group>"; };
 		3870FF4225EC13F40088248F /* BloodGlucose.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BloodGlucose.swift; sourceTree = "<group>"; };
 		3871F39B25ED892B0013ECB5 /* TempTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTarget.swift; sourceTree = "<group>"; };
@@ -848,7 +844,6 @@
 		38A0363A25ECF07E00FCBB52 /* GlucoseStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseStorage.swift; sourceTree = "<group>"; };
 		38A0364125ED069400FCBB52 /* TempBasal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempBasal.swift; sourceTree = "<group>"; };
 		38A13D3125E28B4B00EAA382 /* PumpHistoryEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpHistoryEvent.swift; sourceTree = "<group>"; };
-		38A43597262E0E4900E80935 /* FetchAnnouncementsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchAnnouncementsManager.swift; sourceTree = "<group>"; };
 		38A5049125DD9C4000C5B9E8 /* UserDefaultsExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefaultsExtensions.swift; sourceTree = "<group>"; };
 		38A9260425F012D8009E3739 /* CarbRatios.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbRatios.swift; sourceTree = "<group>"; };
 		38AAF85425FFF846004AF583 /* CurrentGlucoseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentGlucoseView.swift; sourceTree = "<group>"; };
@@ -1195,6 +1190,7 @@
 		DD9ECB732CA9A0C300AA7C45 /* RemoteControlConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfig.swift; sourceTree = "<group>"; };
 		DDA6E31F2D258E0500C2988C /* OverrideHelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideHelpView.swift; sourceTree = "<group>"; };
 		DDA6E3212D25901100C2988C /* TempTargetHelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetHelpView.swift; sourceTree = "<group>"; };
+		DDA6E24F2D22187500C2988C /* ChartLegendView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartLegendView.swift; sourceTree = "<group>"; };
 		DDB37CC22D05044D00D99BF4 /* ContactTrickEntryStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContactTrickEntryStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		DDB37CC32D05044D00D99BF4 /* ContactTrickEntryStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContactTrickEntryStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		DDB37CC42D05048F00D99BF4 /* ContactImageStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactImageStorage.swift; sourceTree = "<group>"; };
@@ -1826,7 +1822,6 @@
 				CE95BF562BA5F5FE00DC3DE3 /* PluginManager.swift */,
 				3811DF0F25CAAAE200A708ED /* APSManager.swift */,
 				38BF021E25E7F0DE00579895 /* DeviceDataManager.swift */,
-				38A43597262E0E4900E80935 /* FetchAnnouncementsManager.swift */,
 				38DAB289260D349500F74C1A /* FetchGlucoseManager.swift */,
 				38192E06261BA9960094D973 /* FetchTreatmentsManager.swift */,
 				3856933F270B57A00002C50D /* CGM */,
@@ -1891,6 +1886,7 @@
 		3833B51E260264AC003021B3 /* Chart */ = {
 			isa = PBXGroup;
 			children = (
+				DDA6E24F2D22187500C2988C /* ChartLegendView.swift */,
 				BD3CC0712B0B89D50013189E /* MainChartView.swift */,
 				BDDAF9F12D0055CC00B34E7A /* ChartElements */,
 			);
@@ -2003,7 +1999,6 @@
 				DD07CA132CE80B73002D45A9 /* TimeInRangeChartStyle.swift */,
 				DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */,
 				DD6D67E32C9C253500660C9B /* ColorSchemeOption.swift */,
-				385CEAC025F2EA52002D6D5B /* Announcement.swift */,
 				388E5A5F25B6F2310019842D /* Autosens.swift */,
 				38A00B1E25FC00F7006BC0B0 /* Autotune.swift */,
 				388358C725EEF6D200E024B2 /* BasalProfileEntry.swift */,
@@ -2097,7 +2092,6 @@
 			isa = PBXGroup;
 			children = (
 				CE82E02428E867BA00473A9C /* AlertStorage.swift */,
-				385CEAC325F2F154002D6D5B /* AnnouncementsStorage.swift */,
 				38AEE75625F0F18E0013F05B /* CarbsStorage.swift */,
 				DDB37CC42D05048F00D99BF4 /* ContactImageStorage.swift */,
 				5864E8582C42CFAE00294306 /* DeterminationStorage.swift */,
@@ -3436,7 +3430,6 @@
 				5825A1BE2C97335C0046467E /* EditTempTargetForm.swift in Sources */,
 				19D466A329AA2B80004D5F33 /* MealSettingsDataFlow.swift in Sources */,
 				3811DEB225C9D88300A708ED /* KeychainItemAccessibility.swift in Sources */,
-				385CEAC425F2F154002D6D5B /* AnnouncementsStorage.swift in Sources */,
 				38AEE73D25F0200C0013F05B /* FreeAPSSettings.swift in Sources */,
 				38FCF3FD25E997A80078B0D1 /* PumpHistoryStorage.swift in Sources */,
 				58645BA72CA2D390008AFCE7 /* ChartAxisSetup.swift in Sources */,
@@ -3558,6 +3551,7 @@
 				DD1745482C55C61D00211FAC /* AutosensSettingsStateModel.swift in Sources */,
 				DD1745462C55C61500211FAC /* AutosensSettingsProvider.swift in Sources */,
 				DDA6E3202D258E0500C2988C /* OverrideHelpView.swift in Sources */,
+				DDA6E2502D22187500C2988C /* ChartLegendView.swift in Sources */,
 				3811DEAF25C9D88300A708ED /* KeyValueStorage.swift in Sources */,
 				DDD6D4D32CDE90720029439A /* HbA1cDisplayUnit.swift in Sources */,
 				38FE826D25CC8461001FF17A /* NightscoutAPI.swift in Sources */,
@@ -3692,7 +3686,6 @@
 				9825E5E923F0B8FA80C8C7C7 /* NightscoutConfigStateModel.swift in Sources */,
 				DDA6E3222D25901100C2988C /* TempTargetHelpView.swift in Sources */,
 				58645B9D2CA2D275008AFCE7 /* DeterminationSetup.swift in Sources */,
-				38A43598262E0E4900E80935 /* FetchAnnouncementsManager.swift in Sources */,
 				DD1745442C55C60E00211FAC /* AutosensSettingsDataFlow.swift in Sources */,
 				BDCAF2382C639F35002DC907 /* SettingItems.swift in Sources */,
 				58D08B342C8DF9A700AA37D3 /* CobIobChart.swift in Sources */,
@@ -3720,7 +3713,6 @@
 				DD1745242C55526000211FAC /* SMBSettingsStateModel.swift in Sources */,
 				F90692D1274B99B60037068D /* HealthKitProvider.swift in Sources */,
 				19F95FF729F10FEE00314DDC /* StatStateModel.swift in Sources */,
-				385CEAC125F2EA52002D6D5B /* Announcement.swift in Sources */,
 				8B759CFCF47B392BB365C251 /* BasalProfileEditorDataFlow.swift in Sources */,
 				195D80B42AF6973A00D25097 /* DynamicSettingsRootView.swift in Sources */,
 				389442CB25F65F7100FA1F27 /* NightscoutTreatment.swift in Sources */,

+ 0 - 1
FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json

@@ -1,7 +1,6 @@
 {
   "units" : "mg/dL",
   "closedLoop" : false,
-  "allowAnnouncements" : false,
   "useAutotune" : false,
   "onlyAutotuneBasals" : false,
   "isUploadEnabled" : false,

+ 0 - 97
FreeAPS/Sources/APS/APSManager.swift

@@ -28,7 +28,6 @@ protocol APSManager {
     func roundBolus(amount: Decimal) -> Decimal
     var lastError: CurrentValueSubject<Error?, Never> { get }
     func cancelBolus() async
-    func enactAnnouncement(_ announcement: Announcement)
 }
 
 enum APSError: LocalizedError {
@@ -64,7 +63,6 @@ final class BaseAPSManager: APSManager, Injectable {
     @Injected() private var alertHistoryStorage: AlertHistoryStorage!
     @Injected() private var tempTargetsStorage: TempTargetsStorage!
     @Injected() private var carbsStorage: CarbsStorage!
-    @Injected() private var announcementsStorage: AnnouncementsStorage!
     @Injected() private var determinationStorage: DeterminationStorage!
     @Injected() private var deviceDataManager: DeviceDataManager!
     @Injected() private var nightscout: NightscoutManager!
@@ -557,101 +555,6 @@ final class BaseAPSManager: APSManager, Injectable {
         await openAPS.autotune()
     }
 
-    func enactAnnouncement(_ announcement: Announcement) {
-        guard let action = announcement.action else {
-            warning(.apsManager, "Invalid Announcement action")
-            return
-        }
-
-        guard let pump = pumpManager else {
-            warning(.apsManager, "Pump is not set")
-            return
-        }
-
-        debug(.apsManager, "Start enact announcement: \(action)")
-
-        switch action {
-        case let .bolus(amount):
-            if let error = verifyStatus() {
-                processError(error)
-                return
-            }
-            let roundedAmount = pump.roundToSupportedBolusVolume(units: Double(amount))
-            pump.enactBolus(units: roundedAmount, activationType: .manualRecommendationAccepted) { error in
-                if let error = error {
-                    // warning(.apsManager, "Announcement Bolus failed with error: \(error.localizedDescription)")
-                    switch error {
-                    case .uncertainDelivery:
-                        // Do not generate notification on uncertain delivery error
-                        break
-                    default:
-                        // Do not generate notifications for automatic boluses that fail.
-                        warning(.apsManager, "Announcement Bolus failed with error: \(error.localizedDescription)")
-                    }
-
-                } else {
-                    debug(.apsManager, "Announcement Bolus succeeded")
-                    self.announcementsStorage.storeAnnouncements([announcement], enacted: true)
-                    self.bolusProgress.send(0)
-                }
-            }
-        case let .pump(pumpAction):
-            switch pumpAction {
-            case .suspend:
-                if let error = verifyStatus() {
-                    processError(error)
-                    return
-                }
-                pump.suspendDelivery { error in
-                    if let error = error {
-                        debug(.apsManager, "Pump not suspended by Announcement: \(error.localizedDescription)")
-                    } else {
-                        debug(.apsManager, "Pump suspended by Announcement")
-                        self.announcementsStorage.storeAnnouncements([announcement], enacted: true)
-                    }
-                }
-            case .resume:
-                guard pump.status.pumpStatus.suspended else {
-                    return
-                }
-                pump.resumeDelivery { error in
-                    if let error = error {
-                        warning(.apsManager, "Pump not resumed by Announcement: \(error.localizedDescription)")
-                    } else {
-                        debug(.apsManager, "Pump resumed by Announcement")
-                        self.announcementsStorage.storeAnnouncements([announcement], enacted: true)
-                    }
-                }
-            }
-        case let .looping(closedLoop):
-            settings.closedLoop = closedLoop
-            debug(.apsManager, "Closed loop \(closedLoop) by Announcement")
-            announcementsStorage.storeAnnouncements([announcement], enacted: true)
-        case let .tempbasal(rate, duration):
-            if let error = verifyStatus() {
-                processError(error)
-                return
-            }
-            // unable to do temp basal during manual temp basal 😁
-            if isManualTempBasal {
-                processError(APSError.manualBasalTemp(message: "Loop not possible during the manual basal temp"))
-                return
-            }
-            guard !settings.closedLoop else {
-                return
-            }
-            let roundedRate = pump.roundToSupportedBasalRate(unitsPerHour: Double(rate))
-            pump.enactTempBasal(unitsPerHour: roundedRate, for: TimeInterval(duration) * 60) { error in
-                if let error = error {
-                    warning(.apsManager, "Announcement TempBasal failed with error: \(error.localizedDescription)")
-                } else {
-                    debug(.apsManager, "Announcement TempBasal succeeded")
-                    self.announcementsStorage.storeAnnouncements([announcement], enacted: true)
-                }
-            }
-        }
-    }
-
     private func fetchCurrentTempBasal(date: Date) async -> TempBasal {
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: PumpEventStored.self,

+ 0 - 52
FreeAPS/Sources/APS/FetchAnnouncementsManager.swift

@@ -1,52 +0,0 @@
-import Combine
-import Foundation
-import SwiftDate
-import Swinject
-
-protocol FetchAnnouncementsManager {}
-
-final class BaseFetchAnnouncementsManager: FetchAnnouncementsManager, Injectable {
-    private let processQueue = DispatchQueue(label: "BaseFetchAnnouncementsManager.processQueue")
-    @Injected() var announcementsStorage: AnnouncementsStorage!
-    @Injected() var nightscoutManager: NightscoutManager!
-    @Injected() var apsManager: APSManager!
-    @Injected() var settingsManager: SettingsManager!
-
-    private var lifetime = Lifetime()
-    private let timer = DispatchTimer(timeInterval: 5.minutes.timeInterval)
-
-    init(resolver: Resolver) {
-        injectServices(resolver)
-        subscribe()
-    }
-
-    private func subscribe() {
-        timer.publisher
-            .receive(on: processQueue)
-            .flatMap { _ -> AnyPublisher<[Announcement], Never> in
-                guard self.settingsManager.settings.allowAnnouncements else {
-                    return Just([]).eraseToAnyPublisher()
-                }
-                debug(.nightscout, "FetchAnnouncementsManager heartbeat")
-                debug(.nightscout, "Start fetching announcements")
-                return self.nightscoutManager.fetchAnnouncements()
-            }
-            .sink { announcements in
-                guard let last = announcements.filter({ $0.createdAt > self.announcementsStorage.syncDate() })
-                    .sorted(by: { $0.createdAt < $1.createdAt })
-                    .last
-                else { return }
-
-                self.announcementsStorage.storeAnnouncements([last], enacted: false)
-                if self.settingsManager.settings.allowAnnouncements, let recent = self.announcementsStorage.recent(),
-                   recent.action != nil
-                {
-                    debug(.nightscout, "New announcements found")
-                    self.apsManager.enactAnnouncement(recent)
-                }
-            }
-            .store(in: &lifetime)
-        timer.fire()
-        timer.resume()
-    }
-}

+ 0 - 2
FreeAPS/Sources/APS/OpenAPS/Constants.swift

@@ -92,8 +92,6 @@ extension OpenAPS {
 
     enum FreeAPS {
         static let settings = "freeaps/freeaps_settings.json"
-        static let announcements = "freeaps/announcements.json"
-        static let announcementsEnacted = "freeaps/announcements_enacted.json"
         static let tempTargetsPresets = "freeaps/temptargets_presets.json"
         static let calibrations = "freeaps/calibrations.json"
     }

+ 0 - 80
FreeAPS/Sources/APS/Storage/AnnouncementsStorage.swift

@@ -1,80 +0,0 @@
-import Foundation
-import SwiftDate
-import Swinject
-
-protocol AnnouncementsStorage {
-    func storeAnnouncements(_ announcements: [Announcement], enacted: Bool)
-    func syncDate() -> Date
-    func recent() -> Announcement?
-    func validate() -> [Announcement]
-}
-
-final class BaseAnnouncementsStorage: AnnouncementsStorage, Injectable {
-    enum Config {
-        static let recentInterval = 10.minutes.timeInterval
-    }
-
-    private let processQueue = DispatchQueue(label: "BaseAnnouncementsStorage.processQueue")
-    @Injected() private var storage: FileStorage!
-
-    init(resolver: Resolver) {
-        injectServices(resolver)
-    }
-
-    func storeAnnouncements(_ announcements: [Announcement], enacted: Bool) {
-        processQueue.sync {
-            let file = enacted ? OpenAPS.FreeAPS.announcementsEnacted : OpenAPS.FreeAPS.announcements
-            self.storage.transaction { storage in
-                storage.append(announcements, to: file, uniqBy: \.createdAt)
-                let uniqEvents = storage.retrieve(file, as: [Announcement].self)?
-                    .filter { $0.createdAt.addingTimeInterval(1.days.timeInterval) > Date() }
-                    .sorted { $0.createdAt > $1.createdAt } ?? []
-                storage.save(Array(uniqEvents), as: file)
-            }
-        }
-    }
-
-    func syncDate() -> Date {
-        guard let events = storage.retrieve(OpenAPS.FreeAPS.announcementsEnacted, as: [Announcement].self),
-              let recentEnacted = events.filter({ $0.enteredBy == Announcement.remote }).first
-        else {
-            return Date().addingTimeInterval(-Config.recentInterval)
-        }
-        return recentEnacted.createdAt.addingTimeInterval(Config.recentInterval)
-    }
-
-    func recent() -> Announcement? {
-        guard let events = storage.retrieve(OpenAPS.FreeAPS.announcements, as: [Announcement].self)
-        else {
-            return nil
-        }
-        guard let recent = events
-            .filter({
-                $0.enteredBy == Announcement.remote && $0.createdAt.addingTimeInterval(Config.recentInterval) > Date()
-            })
-            .first
-        else {
-            return nil
-        }
-        guard let enactedEvents = storage.retrieve(OpenAPS.FreeAPS.announcementsEnacted, as: [Announcement].self)
-        else {
-            return recent
-        }
-
-        guard enactedEvents.first(where: { $0.createdAt == recent.createdAt }) == nil
-        else {
-            return nil
-        }
-        return recent
-    }
-
-    func validate() -> [Announcement] {
-        guard let enactedEvents = storage.retrieve(OpenAPS.FreeAPS.announcementsEnacted, as: [Announcement].self)?.reversed()
-        else {
-            return []
-        }
-        let validate = enactedEvents
-            .filter({ $0.enteredBy == Announcement.remote })
-        return validate
-    }
-}

+ 1 - 1
FreeAPS/Sources/APS/Storage/OverrideStorage.swift

@@ -218,7 +218,7 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
             guard let fetchedOverrides = results as? [OverrideStored] else { return [] }
 
             return fetchedOverrides.map { override in
-                let duration = override.indefinite ? 1440 : override.duration ?? 0 // 1440 min = 1 day
+                let duration = override.indefinite ? 43200 : override.duration ?? 0 // 43200 min = 30 days
                 return NightscoutExercise(
                     duration: Int(truncating: duration),
                     eventType: OverrideStored.EventType.nsExercise,

+ 0 - 1
FreeAPS/Sources/Application/FreeAPSApp.swift

@@ -46,7 +46,6 @@ import Swinject
         _ = resolver.resolve(APSManager.self)!
         _ = resolver.resolve(FetchGlucoseManager.self)!
         _ = resolver.resolve(FetchTreatmentsManager.self)!
-        _ = resolver.resolve(FetchAnnouncementsManager.self)!
         _ = resolver.resolve(CalendarManager.self)!
         _ = resolver.resolve(UserNotificationsManager.self)!
         _ = resolver.resolve(WatchManager.self)!

+ 0 - 1
FreeAPS/Sources/Assemblies/APSAssembly.swift

@@ -7,7 +7,6 @@ final class APSAssembly: Assembly {
         container.register(APSManager.self) { r in BaseAPSManager(resolver: r) }
         container.register(FetchGlucoseManager.self) { r in BaseFetchGlucoseManager(resolver: r) }
         container.register(FetchTreatmentsManager.self) { r in BaseFetchTreatmentsManager(resolver: r) }
-        container.register(FetchAnnouncementsManager.self) { r in BaseFetchAnnouncementsManager(resolver: r) }
         container.register(BluetoothStateManager.self) { r in BaseBluetoothStateManager(resolver: r) }
         container.register(PluginManager.self) { r in BasePluginManager(resolver: r) }
         container.register(CalibrationService.self) { r in BaseCalibrationService(resolver: r) }

+ 0 - 1
FreeAPS/Sources/Assemblies/StorageAssembly.swift

@@ -14,7 +14,6 @@ final class StorageAssembly: Assembly {
         container.register(TempTargetsStorage.self) { r in BaseTempTargetsStorage(resolver: r) }
         container.register(CarbsStorage.self) { r in BaseCarbsStorage(resolver: r) }
         container.register(ContactImageStorage.self) { r in BaseContactImageStorage(resolver: r) }
-        container.register(AnnouncementsStorage.self) { r in BaseAnnouncementsStorage(resolver: r) }
         container.register(SettingsManager.self) { r in BaseSettingsManager(resolver: r) }
         container.register(Keychain.self) { _ in BaseKeychain() }
         container.register(AlertHistoryStorage.self) { r in BaseAlertHistoryStorage(resolver: r) }

+ 0 - 6
FreeAPS/Sources/Localizations/Main/ar.lproj/Localizable.strings

@@ -1235,12 +1235,6 @@ Enact a temp Basal or a temp target */
 /* Debug option view Enacted */
 "Enacted" = "Enacted";
 
-/* Debug option view Announcements (from NS) */
-"Announcements" = "Announcements";
-
-/* Debug option view Enacted announcements announcements (from NS) */
-"Enacted announcements" = "Enacted announcements";
-
 /* Debug option view Autotune */
 "Autotune" = "Autotune";
 

+ 0 - 6
FreeAPS/Sources/Localizations/Main/ca.lproj/Localizable.strings

@@ -1086,12 +1086,6 @@ Enact a temp Basal or a temp target */
 /* Debug option view Enacted */
 "Enacted" = "Enacted";
 
-/* Debug option view Announcements (from NS) */
-"Announcements" = "Announcements";
-
-/* Debug option view Enacted announcements announcements (from NS) */
-"Enacted announcements" = "Enacted announcements";
-
 /* Debug option view Autotune */
 "Autotune" = "Autotune";
 

+ 0 - 6
FreeAPS/Sources/Localizations/Main/da.lproj/Localizable.strings

@@ -1196,12 +1196,6 @@ Enact a temp Basal or a temp target */
 /* Debug option view Enacted */
 "Enacted" = "Udført";
 
-/* Debug option view Announcements (from NS) */
-"Announcements" = "Announcements";
-
-/* Debug option view Enacted announcements announcements (from NS) */
-"Enacted announcements" = "Enacted announcements";
-
 /* Debug option view Autotune */
 "Autotune" = "Autotune";
 

+ 0 - 6
FreeAPS/Sources/Localizations/Main/de.lproj/Localizable.strings

@@ -1241,12 +1241,6 @@ Enact a temp Basal or a temp target */
 /* Debug option view Enacted */
 "Enacted" = "letzte Berechnung";
 
-/* Debug option view Announcements (from NS) */
-"Announcements" = "Notifikationen";
-
-/* Debug option view Enacted announcements announcements (from NS) */
-"Enacted announcements" = "Erlassene Ankündigungen";
-
 /* Debug option view Autotune */
 "Autotune" = "Autotune";
 

+ 0 - 6
FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings

@@ -1238,12 +1238,6 @@ Enact a temp Basal or a temp target */
 /* Debug option view Enacted */
 "Enacted" = "Enacted";
 
-/* Debug option view Announcements (from NS) */
-"Announcements" = "Announcements";
-
-/* Debug option view Enacted announcements announcements (from NS) */
-"Enacted announcements" = "Enacted announcements";
-
 /* Debug option view Autotune */
 "Autotune" = "Autotune";
 

+ 0 - 6
FreeAPS/Sources/Localizations/Main/es.lproj/Localizable.strings

@@ -1234,12 +1234,6 @@ Enact a temp Basal or a temp target */
 /* Debug option view Enacted */
 "Enacted" = "Enacted";
 
-/* Debug option view Announcements (from NS) */
-"Announcements" = "Announcements";
-
-/* Debug option view Enacted announcements announcements (from NS) */
-"Enacted announcements" = "Enacted announcements";
-
 /* Debug option view Autotune */
 "Autotune" = "Autotune";
 

+ 0 - 6
FreeAPS/Sources/Localizations/Main/fi.lproj/Localizable.strings

@@ -1241,12 +1241,6 @@ Enact a temp Basal or a temp target */
 /* Debug option view Enacted */
 "Enacted" = "Enacted";
 
-/* Debug option view Announcements (from NS) */
-"Announcements" = "Announcements";
-
-/* Debug option view Enacted announcements announcements (from NS) */
-"Enacted announcements" = "Enacted announcements";
-
 /* Debug option view Autotune */
 "Autotune" = "Autotune";
 

+ 0 - 6
FreeAPS/Sources/Localizations/Main/fr.lproj/Localizable.strings

@@ -1235,12 +1235,6 @@ Enact a temp Basal or a temp target */
 /* Debug option view Enacted */
 "Enacted" = "Activé";
 
-/* Debug option view Announcements (from NS) */
-"Announcements" = "Annonces";
-
-/* Debug option view Enacted announcements announcements (from NS) */
-"Enacted announcements" = "Annonces émises";
-
 /* Debug option view Autotune */
 "Autotune" = "Autotune";
 

+ 0 - 6
FreeAPS/Sources/Localizations/Main/he.lproj/Localizable.strings

@@ -1235,12 +1235,6 @@ Enact a temp Basal or a temp target */
 /* Debug option view Enacted */
 "Enacted" = "Enacted";
 
-/* Debug option view Announcements (from NS) */
-"Announcements" = "Announcements";
-
-/* Debug option view Enacted announcements announcements (from NS) */
-"Enacted announcements" = "Enacted announcements";
-
 /* Debug option view Autotune */
 "Autotune" = "Autotune";
 

+ 0 - 6
FreeAPS/Sources/Localizations/Main/hu.lproj/Localizable.strings

@@ -1193,12 +1193,6 @@ Enact a temp Basal or a temp target */
 /* Debug option view Enacted */
 "Enacted" = "Enacted";
 
-/* Debug option view Announcements (from NS) */
-"Announcements" = "Announcements";
-
-/* Debug option view Enacted announcements announcements (from NS) */
-"Enacted announcements" = "Enacted announcements";
-
 /* Debug option view Autotune */
 "Autotune" = "Autotune";
 

+ 0 - 6
FreeAPS/Sources/Localizations/Main/it.lproj/Localizable.strings

@@ -1235,12 +1235,6 @@ Enact a temp Basal or a temp target */
 /* Debug option view Enacted */
 "Enacted" = "Debug Attivato";
 
-/* Debug option view Announcements (from NS) */
-"Announcements" = "Annunci";
-
-/* Debug option view Enacted announcements announcements (from NS) */
-"Enacted announcements" = "Annunci attivati";
-
 /* Debug option view Autotune */
 "Autotune" = "Autotune";
 

+ 0 - 6
FreeAPS/Sources/Localizations/Main/nb.lproj/Localizable.strings

@@ -1235,12 +1235,6 @@ Enact a temp Basal or a temp target */
 /* Debug option view Enacted */
 "Enacted" = "Utført";
 
-/* Debug option view Announcements (from NS) */
-"Announcements" = "Kunngjøringer";
-
-/* Debug option view Enacted announcements announcements (from NS) */
-"Enacted announcements" = "Utførte kunngjøringer";
-
 /* Debug option view Autotune */
 "Autotune" = "Autotune";
 

+ 0 - 6
FreeAPS/Sources/Localizations/Main/nl.lproj/Localizable.strings

@@ -1235,12 +1235,6 @@ Enact a temp Basal or a temp target */
 /* Debug option view Enacted */
 "Enacted" = "Vastgesteld";
 
-/* Debug option view Announcements (from NS) */
-"Announcements" = "Meldingen";
-
-/* Debug option view Enacted announcements announcements (from NS) */
-"Enacted announcements" = "Ingevoerde meldingen";
-
 /* Debug option view Autotune */
 "Autotune" = "Autotune";
 

+ 0 - 6
FreeAPS/Sources/Localizations/Main/pl.lproj/Localizable.strings

@@ -1237,12 +1237,6 @@ Połączono z Nightscout!";
 /* Debug option view Enacted */
 "Enacted" = "Enacted";
 
-/* Debug option view Announcements (from NS) */
-"Announcements" = "Announcements";
-
-/* Debug option view Enacted announcements announcements (from NS) */
-"Enacted announcements" = "Enacted announcements";
-
 /* Debug option view Autotune */
 "Autotune" = "Autotune";
 

+ 0 - 6
FreeAPS/Sources/Localizations/Main/pt-BR.lproj/Localizable.strings

@@ -1235,12 +1235,6 @@ Enact a temp Basal or a temp target */
 /* Debug option view Enacted */
 "Enacted" = "Enacted";
 
-/* Debug option view Announcements (from NS) */
-"Announcements" = "Announcements";
-
-/* Debug option view Enacted announcements announcements (from NS) */
-"Enacted announcements" = "Enacted announcements";
-
 /* Debug option view Autotune */
 "Autotune" = "Autotune";
 

+ 0 - 6
FreeAPS/Sources/Localizations/Main/pt-PT.lproj/Localizable.strings

@@ -1235,12 +1235,6 @@ Enact a temp Basal or a temp target */
 /* Debug option view Enacted */
 "Enacted" = "Enacted";
 
-/* Debug option view Announcements (from NS) */
-"Announcements" = "Announcements";
-
-/* Debug option view Enacted announcements announcements (from NS) */
-"Enacted announcements" = "Enacted announcements";
-
 /* Debug option view Autotune */
 "Autotune" = "Autotune";
 

+ 0 - 6
FreeAPS/Sources/Localizations/Main/ru.lproj/Localizable.strings

@@ -1238,12 +1238,6 @@ Enact a temp Basal or a temp target */
 /* Debug option view Enacted */
 "Enacted" = "Принято";
 
-/* Debug option view Announcements (from NS) */
-"Announcements" = "Оповещения";
-
-/* Debug option view Enacted announcements announcements (from NS) */
-"Enacted announcements" = "Принятые оповещения";
-
 /* Debug option view Autotune */
 "Autotune" = "Автотюн";
 

+ 0 - 6
FreeAPS/Sources/Localizations/Main/sk.lproj/Localizable.strings

@@ -1193,12 +1193,6 @@ Enact a temp Basal or a temp target */
 /* Debug option view Enacted */
 "Enacted" = "Prijaté";
 
-/* Debug option view Announcements (from NS) */
-"Announcements" = "Oznámenia";
-
-/* Debug option view Enacted announcements announcements (from NS) */
-"Enacted announcements" = "Prijaté oznámenia";
-
 /* Debug option view Autotune */
 "Autotune" = "Autotune";
 

+ 0 - 6
FreeAPS/Sources/Localizations/Main/sv.lproj/Localizable.strings

@@ -1235,12 +1235,6 @@ Enact a temp Basal or a temp target */
 /* Debug option view Enacted */
 "Enacted" = "Utfört";
 
-/* Debug option view Announcements (from NS) */
-"Announcements" = "Meddelanden (via NS)";
-
-/* Debug option view Enacted announcements announcements (from NS) */
-"Enacted announcements" = "Överförda meddelanden";
-
 /* Debug option view Autotune */
 "Autotune" = "Autotune";
 

+ 0 - 6
FreeAPS/Sources/Localizations/Main/tr.lproj/Localizable.strings

@@ -1239,12 +1239,6 @@ Enact a temp Basal or a temp target */
 /* Debug option view Enacted */
 "Enacted" = "Enacted";
 
-/* Debug option view Announcements (from NS) */
-"Announcements" = "Duyurular";
-
-/* Debug option view Enacted announcements announcements (from NS) */
-"Enacted announcements" = "Enacted announcements";
-
 /* Debug option view Autotune */
 "Autotune" = "Otoayar";
 

+ 0 - 6
FreeAPS/Sources/Localizations/Main/uk.lproj/Localizable.strings

@@ -1235,12 +1235,6 @@ Enact a temp Basal or a temp target */
 /* Debug option view Enacted */
 "Enacted" = "Введено в дію";
 
-/* Debug option view Announcements (from NS) */
-"Announcements" = "Оголошення";
-
-/* Debug option view Enacted announcements announcements (from NS) */
-"Enacted announcements" = "Введені в дію оголошення";
-
 /* Debug option view Autotune */
 "Autotune" = "Автотюн";
 

+ 0 - 6
FreeAPS/Sources/Localizations/Main/vi.lproj/Localizable.strings

@@ -1193,12 +1193,6 @@ Enact a temp Basal or a temp target */
 /* Debug option view Enacted */
 "Enacted" = "Đã kích hoạt";
 
-/* Debug option view Announcements (from NS) */
-"Announcements" = "Các thông báo";
-
-/* Debug option view Enacted announcements announcements (from NS) */
-"Enacted announcements" = "Thông báo đã kích hoạt";
-
 /* Debug option view Autotune */
 "Autotune" = "Autotune";
 

+ 0 - 6
FreeAPS/Sources/Localizations/Main/zh-Hans.lproj/Localizable.strings

@@ -1235,12 +1235,6 @@ Enact a temp Basal or a temp target */
 /* Debug option view Enacted */
 "Enacted" = "Enacted";
 
-/* Debug option view Announcements (from NS) */
-"Announcements" = "Announcements";
-
-/* Debug option view Enacted announcements announcements (from NS) */
-"Enacted announcements" = "Enacted announcements";
-
 /* Debug option view Autotune */
 "Autotune" = "自动调谐";
 

+ 0 - 57
FreeAPS/Sources/Models/Announcement.swift

@@ -1,57 +0,0 @@
-import Foundation
-
-struct Announcement: JSON, Equatable, Hashable {
-    let createdAt: Date
-    let enteredBy: String
-    let notes: String
-
-    static let remote = "remote"
-
-    var action: AnnouncementAction? {
-        let components = notes.replacingOccurrences(of: " ", with: "").split(separator: ":")
-        guard components.count == 2 else {
-            return nil
-        }
-        let command = String(components[0]).lowercased()
-        let arguments = String(components[1]).lowercased()
-        switch command {
-        case "bolus":
-            guard let amount = Decimal(from: arguments) else { return nil }
-            return .bolus(amount)
-        case "pump":
-            guard let action = PumpAction(rawValue: arguments) else { return nil }
-            return .pump(action)
-        case "looping":
-            guard let looping = Bool(from: arguments) else { return nil }
-            return .looping(looping)
-        case "tempbasal":
-            let basalComponents = arguments.split(separator: ",")
-            guard basalComponents.count == 2 else { return nil }
-            let rateArg = String(basalComponents[0])
-            let durationArg = String(basalComponents[1])
-            guard let rate = Decimal(from: rateArg), let duration = Decimal(from: durationArg) else { return nil }
-            return .tempbasal(rate: rate, duration: duration)
-        default: return nil
-        }
-    }
-}
-
-extension Announcement {
-    private enum CodingKeys: String, CodingKey {
-        case createdAt = "created_at"
-        case enteredBy
-        case notes
-    }
-}
-
-enum AnnouncementAction {
-    case bolus(Decimal)
-    case pump(PumpAction)
-    case looping(Bool)
-    case tempbasal(rate: Decimal, duration: Decimal)
-}
-
-enum PumpAction: String {
-    case suspend
-    case resume
-}

+ 0 - 5
FreeAPS/Sources/Models/FreeAPSSettings.swift

@@ -18,7 +18,6 @@ enum BolusShortcutLimit: String, JSON, CaseIterable, Identifiable {
 struct FreeAPSSettings: JSON, Equatable {
     var units: GlucoseUnits = .mgdL
     var closedLoop: Bool = false
-    var allowAnnouncements: Bool = false
     var useAutotune: Bool = false
     var isUploadEnabled: Bool = false
     var isDownloadEnabled: Bool = false
@@ -94,10 +93,6 @@ extension FreeAPSSettings: Decodable {
             settings.closedLoop = closedLoop
         }
 
-        if let allowAnnouncements = try? container.decode(Bool.self, forKey: .allowAnnouncements) {
-            settings.allowAnnouncements = allowAnnouncements
-        }
-
         if let useAutotune = try? container.decode(Bool.self, forKey: .useAutotune) {
             settings.useAutotune = useAutotune
         }

+ 3 - 1
FreeAPS/Sources/Modules/AutosensSettings/AutosensSettingsDataFlow.swift

@@ -2,4 +2,6 @@ enum AutosensSettings {
     enum Config {}
 }
 
-protocol AutosensSettingsProvider: Provider {}
+protocol AutosensSettingsProvider: Provider {
+    var autosense: Autosens { get }
+}

+ 7 - 1
FreeAPS/Sources/Modules/AutosensSettings/AutosensSettingsProvider.swift

@@ -1,3 +1,9 @@
 extension AutosensSettings {
-    final class Provider: BaseProvider, AutosensSettingsProvider {}
+    final class Provider: BaseProvider, AutosensSettingsProvider {
+        var autosense: Autosens {
+            storage.retrieve(OpenAPS.Settings.autosense, as: Autosens.self)
+                ?? Autosens(from: OpenAPS.defaults(for: OpenAPS.Settings.autosense))
+                ?? Autosens(ratio: 1, newisf: nil, timestamp: nil)
+        }
+    }
 }

+ 42 - 0
FreeAPS/Sources/Modules/AutosensSettings/AutosensSettingsStateModel.swift

@@ -1,3 +1,4 @@
+import CoreData
 import Observation
 import SwiftUI
 
@@ -5,19 +6,60 @@ extension AutosensSettings {
     final class StateModel: BaseStateModel<Provider> {
         @Injected() var settings: SettingsManager!
         @Injected() var storage: FileStorage!
+        @Injected() var determinationStorage: DeterminationStorage!
 
         var units: GlucoseUnits = .mgdL
 
+        private(set) var autosensISF: Decimal?
+        private(set) var autosensRatio: Decimal = 0
+        var determinationsFromPersistence: [OrefDetermination] = []
+
+        let viewContext = CoreDataStack.shared.persistentContainer.viewContext
+
         @Published var autosensMax: Decimal = 1.2
         @Published var autosensMin: Decimal = 0.7
         @Published var rewindResetsAutosens: Bool = true
 
+        var preferences: Preferences {
+            settingsManager.preferences
+        }
+
         override func subscribe() {
             units = settingsManager.settings.units
 
             subscribePreferencesSetting(\.autosensMax, on: $autosensMax) { autosensMax = $0 }
             subscribePreferencesSetting(\.autosensMin, on: $autosensMin) { autosensMin = $0 }
             subscribePreferencesSetting(\.rewindResetsAutosens, on: $rewindResetsAutosens) { rewindResetsAutosens = $0 }
+
+            if let newISF = provider.autosense.newisf {
+                autosensISF = newISF
+            }
+
+            autosensRatio = provider.autosense.ratio
+            setupDeterminationsArray()
+        }
+
+        private func setupDeterminationsArray() {
+            Task {
+                let ids = await determinationStorage.fetchLastDeterminationObjectID(
+                    predicate: NSPredicate.enactedDetermination
+                )
+                await updateDeterminationsArray(with: ids)
+            }
+        }
+
+        @MainActor private func updateDeterminationsArray(with IDs: [NSManagedObjectID]) {
+            do {
+                let objects = try IDs.compactMap { id in
+                    try viewContext.existingObject(with: id) as? OrefDetermination
+                }
+                determinationsFromPersistence = objects
+
+            } catch {
+                debugPrint(
+                    "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the glucose array: \(error.localizedDescription)"
+                )
+            }
         }
     }
 }

+ 108 - 0
FreeAPS/Sources/Modules/AutosensSettings/View/AutosensSettingsRootView.swift

@@ -16,8 +16,116 @@ extension AutosensSettings {
         @EnvironmentObject var appIcons: Icons
         @Environment(AppState.self) var appState
 
+        private var rateFormatter: NumberFormatter {
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            formatter.maximumFractionDigits = 2
+            return formatter
+        }
+
+        var autosensVerboseHint: some View {
+            VStack(alignment: .leading, spacing: 15) {
+                Text(
+                    "Autosens automatically adjusts insulin delivery based on how sensitive or resistant you are to insulin at the time of the current loop cycle by analyzing past data to keep blood sugar levels stable."
+                )
+
+                VStack(alignment: .leading, spacing: 5) {
+                    Text("How it Works").bold()
+                    Text(
+                        "It looks at the last 8-24 hours of data, excluding meal-related changes, and adjusts insulin settings like basal rates and targets when needed to match your sensitivity or resistance to insulin."
+                    )
+                }
+
+                VStack(alignment: .leading, spacing: 5) {
+                    Text("What it Adjusts").bold()
+                    Text(
+                        "Autosens modifies Insulin Sensitivity Factor (ISF), basal rates, and target blood sugar levels. It doesn’t account for carbs but adjusts for insulin effectiveness based on patterns in your glucose data."
+                    )
+                }
+
+                VStack(alignment: .leading, spacing: 5) {
+                    Text("Key Limitations").bold()
+                    Text(
+                        "Autosens has safety limits determined by your Autosens Max and Autosens Min settings. These settings prevent over-adjusting."
+                    )
+                }
+
+                Text(
+                    "Autosens functions alongside certain settings, like Super Micro Bolus (SMB). Other settings, like Dynamic ISF, alter portions of the Autosens formula. Please review the in-app hints for the Algorithm Settings prior to enabling them to understand how they may influence it."
+                )
+            }
+        }
+
+        var AutosensView: some View {
+            Section(
+                header: !state.settingsManager.preferences
+                    .useNewFormula ? Text("Autosens") : Text("Dynamic Sensitivity")
+            ) {
+                VStack {
+                    let dynamicRatio = state.determinationsFromPersistence.first?.sensitivityRatio
+                    let dynamicISF = state.determinationsFromPersistence.first?.insulinSensitivity
+                    let newISF = state.autosensISF
+                    HStack {
+                        Text("Sensitivity Ratio")
+                        Spacer()
+                        Text(
+                            rateFormatter
+                                .string(from: (
+                                    (
+                                        !state.settingsManager.preferences.useNewFormula ? state
+                                            .autosensRatio as NSDecimalNumber : dynamicRatio
+                                    ) ?? 1
+                                ) as NSNumber) ?? "1"
+                        )
+                    }.padding(.vertical)
+                    HStack {
+                        Text("Calculated Sensitivity")
+                        Spacer()
+                        if state.units == .mgdL {
+                            Text(
+                                !state.settingsManager.preferences
+                                    .useNewFormula ? newISF!.description : (dynamicISF ?? 0).description
+                            )
+                        } else {
+                            Text((
+                                !state.settingsManager.preferences
+                                    .useNewFormula ? newISF!.formattedAsMmolL : dynamicISF?.decimalValue.formattedAsMmolL
+                            ) ?? "0")
+                        }
+                        Text(state.units.rawValue + "/U").foregroundColor(.secondary)
+                    }
+
+                    HStack(alignment: .top) {
+                        Text(
+                            "This adjusted ISF is temporary, will change with the next loop cycle, and should not be directly used as your profile ISF value."
+                        )
+                        .font(.footnote)
+                        .foregroundColor(.secondary)
+                        .lineLimit(nil)
+                        Spacer()
+                        Button(
+                            action: {
+                                hintLabel = "Autosens"
+                                selectedVerboseHint = AnyView(autosensVerboseHint)
+                                shouldDisplayHint.toggle()
+                            },
+                            label: {
+                                HStack {
+                                    Image(systemName: "questionmark.circle")
+                                }
+                            }
+                        ).buttonStyle(BorderlessButtonStyle())
+                    }.padding(.top)
+                }.padding(.bottom)
+            }.listRowBackground(Color.chart)
+        }
+
         var body: some View {
             List {
+                if state.autosensISF != nil {
+                    AutosensView
+                }
+
                 SettingInputSection(
                     decimalValue: $state.autosensMax,
                     booleanValue: $booleanPlaceholder,

+ 0 - 1
FreeAPS/Sources/Modules/Home/HomeDataFlow.swift

@@ -13,6 +13,5 @@ protocol HomeProvider: Provider {
     func tempTargets(hours: Int) -> [TempTarget]
     func pumpReservoir() -> Decimal?
     func tempTarget() -> TempTarget?
-    func announcement(_ hours: Int) -> [Announcement]
     func getBGTarget() async -> BGTargets
 }

+ 0 - 7
FreeAPS/Sources/Modules/Home/HomeProvider.swift

@@ -7,7 +7,6 @@ extension Home {
         @Injected() var apsManager: APSManager!
         @Injected() var glucoseStorage: GlucoseStorage!
         @Injected() var tempTargetsStorage: TempTargetsStorage!
-        @Injected() var announcementStorage: AnnouncementsStorage!
 
         func pumpTimeZone() -> TimeZone? {
             apsManager.pumpManager?.status.timeZone
@@ -27,12 +26,6 @@ extension Home {
             tempTargetsStorage.current()
         }
 
-        func announcement(_ hours: Int) -> [Announcement] {
-            announcementStorage.validate().filter {
-                $0.createdAt.addingTimeInterval(hours.hours.timeInterval) > Date()
-            }
-        }
-
         func pumpSettings() -> PumpSettings {
             storage.retrieve(OpenAPS.Settings.settings, as: PumpSettings.self)
                 ?? PumpSettings(from: OpenAPS.defaults(for: OpenAPS.Settings.settings))

+ 0 - 2
FreeAPS/Sources/Modules/Home/HomeStateModel.swift

@@ -20,7 +20,6 @@ extension Home {
         private let timer = DispatchTimer(timeInterval: 5)
         private(set) var filteredHours = 24
         var manualGlucose: [BloodGlucose] = []
-        var announcement: [Announcement] = []
         var uploadStats = false
         var recentGlucose: BloodGlucose?
         var maxBasal: Decimal = 2
@@ -68,7 +67,6 @@ extension Home {
         var totalBolus: Decimal = 0
         var isStatusPopupPresented: Bool = false
         var isLegendPresented: Bool = false
-        var legendSheetDetent = PresentationDetent.large
         var totalInsulinDisplayType: TotalInsulinDisplayType = .totalDailyDose
         var roundedTotalBolus: String = ""
         var selectedTab: Int = 0

+ 46 - 25
FreeAPS/Sources/Modules/Home/View/Chart/ChartElements/BasalChart.swift

@@ -153,7 +153,7 @@ extension MainChartView {
         return tempBasals.compactMap { temp -> (start: Date, end: Date, rate: Double)? in
             let duration = temp.tempBasal?.duration ?? 0
             let timestamp = temp.timestamp ?? Date()
-            let end = min(timestamp + duration.minutes, now)
+            let end = timestamp + duration.minutes
             let isInsulinSuspended = state.suspensions.contains { $0.timestamp ?? now >= timestamp && $0.timestamp ?? now <= end }
 
             let rate = Double(truncating: temp.tempBasal?.rate ?? Decimal.zero as NSDecimalNumber) * (isInsulinSuspended ? 0 : 1)
@@ -168,14 +168,13 @@ extension MainChartView {
 
     func findRegularBasalPoints(
         timeBegin: TimeInterval,
-        timeEnd: TimeInterval,
-        autotuned: Bool
+        timeEnd: TimeInterval
     ) async -> [BasalProfile] {
         guard timeBegin < timeEnd else { return [] }
 
         let beginDate = Date(timeIntervalSince1970: timeBegin)
         let startOfDay = Calendar.current.startOfDay(for: beginDate)
-        let profile = autotuned ? state.autotunedBasalProfile : state.basalProfile
+        let profile = state.basalProfile
         var basalPoints: [BasalProfile] = []
 
         // Iterate over the next three days, multiplying the time intervals
@@ -203,34 +202,56 @@ extension MainChartView {
         Task {
             let dayAgoTime = Date().addingTimeInterval(-1.days.timeInterval).timeIntervalSince1970
 
-            // Get Regular and Autotuned Basal parallel
             async let getRegularBasalPoints = findRegularBasalPoints(
                 timeBegin: dayAgoTime,
-                timeEnd: endMarker.timeIntervalSince1970,
-                autotuned: false
+                timeEnd: endMarker.timeIntervalSince1970
             )
 
-            async let getAutotunedBasalPoints = findRegularBasalPoints(
-                timeBegin: dayAgoTime,
-                timeEnd: endMarker.timeIntervalSince1970,
-                autotuned: true
-            )
+            var regularPoints = await getRegularBasalPoints
+            regularPoints.sort { $0.startDate < $1.startDate }
 
-            let (regularPoints, autotunedBasalPoints) = await (getRegularBasalPoints, getAutotunedBasalPoints)
+            var basals: [BasalProfile] = []
 
-            var totalBasal = regularPoints + autotunedBasalPoints
-            totalBasal.sort {
-                $0.startDate.timeIntervalSince1970 < $1.startDate.timeIntervalSince1970
+            // No basal data? Then there's nothing to draw
+            if regularPoints.isEmpty {
+                // basals stays empty; do nothing
             }
-
-            var basals: [BasalProfile] = []
-            totalBasal.indices.forEach { index in
-                basals.append(BasalProfile(
-                    amount: totalBasal[index].amount,
-                    isOverwritten: totalBasal[index].isOverwritten,
-                    startDate: totalBasal[index].startDate,
-                    endDate: totalBasal.count > index + 1 ? totalBasal[index + 1].startDate : endMarker
-                ))
+            // Exactly one data point?
+            else if regularPoints.count == 1 {
+                let single = regularPoints[0]
+                // Make one BasalProfile that stretches entire marker area
+                basals.append(
+                    BasalProfile(
+                        amount: single.amount,
+                        isOverwritten: single.isOverwritten,
+                        startDate: startMarker,
+                        endDate: endMarker
+                    )
+                )
+            }
+            // Multiple data points: chain them so each point ends where the next begins
+            else {
+                for i in 0 ..< (regularPoints.count - 1) {
+                    basals.append(
+                        BasalProfile(
+                            amount: regularPoints[i].amount,
+                            isOverwritten: regularPoints[i].isOverwritten,
+                            startDate: regularPoints[i].startDate,
+                            endDate: regularPoints[i + 1].startDate
+                        )
+                    )
+                }
+                // The last item goes from its start to endMarker
+                if let lastItem = regularPoints.last {
+                    basals.append(
+                        BasalProfile(
+                            amount: lastItem.amount,
+                            isOverwritten: lastItem.isOverwritten,
+                            startDate: lastItem.startDate,
+                            endDate: endMarker
+                        )
+                    )
+                }
             }
 
             await MainActor.run {

+ 1 - 1
FreeAPS/Sources/Modules/Home/View/Chart/ChartElements/OverrideView.swift

@@ -24,7 +24,7 @@ struct OverrideView: ChartContent {
                 context: viewContext
             ) ?? 0
             let end: Date = duration != 0 ? start.addingTimeInterval(duration) : start
-                .addingTimeInterval(6 * 60 * 60) // handle infinite overrides
+                .addingTimeInterval(60 * 60 * 24 * 30) // handle infinite overrides -> 60s x 60m x 24h x 30d = 30 days duration
 
             let target = getOverrideTarget(override: override)
 

+ 229 - 0
FreeAPS/Sources/Modules/Home/View/Chart/ChartLegendView.swift

@@ -0,0 +1,229 @@
+import SwiftUI
+
+struct ChartLegendView: View {
+    @Environment(AppState.self) var appState
+    @Environment(\.colorScheme) var colorScheme
+
+    var state: Home.StateModel
+
+    @State var legendSheetDetent = PresentationDetent.large
+
+    var body: some View {
+        NavigationStack {
+            VStack(alignment: .leading) {
+                Text(
+                    "The main chart in Trio is made up of various elements and shapes. Find their meanings below."
+                )
+                .font(.subheadline)
+                .foregroundColor(.secondary)
+                .padding(.top, 50)
+
+                List {
+                    VStack(alignment: .leading) {
+                        Text("Forecasts").bold().padding(.bottom, 5).textCase(.uppercase)
+                        Text(
+                            "The oref algorithm determines insulin dosing based on a number of scenarios that it estimates with different types of forecasts."
+                        )
+                        .font(.subheadline)
+                        .foregroundColor(.primary)
+
+                        if state.forecastDisplayType == .lines {
+                            legendLinesView
+                        } else {
+                            legendConeOfUncertaintyView
+                        }
+                    }.listRowBackground(Color.gray.opacity(0.1))
+
+                    VStack(alignment: .leading) {
+                        Text("Other Elements & Shapes").bold().padding(.bottom, 5).textCase(.uppercase)
+
+                        DefinitionRow(
+                            term: "Scheduled Basal Rate",
+                            definition: VStack(alignment: .leading, spacing: 10) {
+                                Text("This dotted line represents the hourly insulin rate of your scheduled basal insulin.")
+                                Text("To review or change your scheduled basal rates, go to Settings > Therapy > Basal Rates.")
+                            },
+                            color: Color.insulin,
+                            iconString: "ellipsis"
+                        )
+
+                        DefinitionRow(
+                            term: "Temporary Basal Rate (TBR)",
+                            definition: Text(
+                                "Shows current or past TBRs, which can be set by the oref algorithm or manually."
+                            ),
+                            color: Color.insulin,
+                            iconString: "square"
+                        )
+
+                        DefinitionRow(
+                            term: "Pump Suspension",
+                            definition: Text("Indicates when insulin delivery was paused, i.e. pump is suspended."),
+                            color: Color.loopGray.opacity(colorScheme == .dark ? 0.3 : 0.8),
+                            iconString: "square.fill"
+                        )
+
+                        DefinitionRow(
+                            term: "CGM Glucose Value",
+                            definition: VStack(alignment: .leading, spacing: 10) {
+                                Text(
+                                    "Displays real-time glucose readings from your CGM. Depending on your user interface settings, this may be displayed in a static (red, green, orange) or dynamic (full color spectrum) coloring scheme."
+                                )
+                                Text(
+                                    "To modify how glucose readings are displayed, go to Settings > Features > User Interface > Glucose Color Scheme."
+                                )
+                            },
+                            color: Color.green,
+                            iconString: !state.settingsManager.settings.smoothGlucose ? "circle.fill" : "record.circle.fill"
+                        )
+
+                        DefinitionRow(
+                            term: "Manual Glucose Measurement",
+                            definition: Text("Manually entered blood glucose, such as a fingerstick test."),
+                            color: Color.red,
+                            iconString: "drop.fill"
+                        )
+
+                        DefinitionRow(
+                            term: "Bolus",
+                            definition: Text(
+                                "Shows an insulin dose, which can be a small automated dose (super-micro-bolus), a manually entered dose, or one given externally (e.g., a pen shot)."
+                            ),
+                            color: Color.insulin,
+                            iconString: "arrowtriangle.down.fill"
+                        )
+
+                        DefinitionRow(
+                            term: "Carb Entry",
+                            definition: Text("Tracks the carbohydrates you eat, entered to guide insulin dosing."),
+                            color: Color.orange,
+                            iconString: "arrowtriangle.down.fill",
+                            shouldRotateIcon: true
+                        )
+
+                        DefinitionRow(
+                            term: "Fat-Protein Carb Equivalent",
+                            definition: VStack(alignment: .leading, spacing: 10) {
+                                Text(
+                                    "Represents carb equivalent for fat and protein, calculated using the Warsaw Method."
+                                )
+                                Text(
+                                    "To enable or configure Warsaw Method application in Trio, go to Settings > Features > Meal Settings."
+                                )
+                            },
+                            color: Color.brown,
+                            iconString: "circle.fill"
+                        )
+
+                        DefinitionRow(
+                            term: "Override",
+                            definition: Text(
+                                "Indicates when an override is or was active, temporarily changing therapy settings (e.g., basal rate, insulin sensitivity, carb ratio, target glucose, or whether Trio can dose SMBs)."
+                            ),
+                            color: Color.purple.opacity(0.4),
+                            iconString: "button.horizontal.fill"
+                        )
+
+                        DefinitionRow(
+                            term: "Temporary Target",
+                            definition: Text(
+                                "Marks when a short-term temporary glucose target is or was active, (potentially) altering when or how much insulin is delivered."
+                            ),
+                            color: Color.green.opacity(0.4),
+                            iconString: "button.horizontal.fill"
+                        )
+
+                        DefinitionRow(
+                            term: "Past Insulin-on-Board (IOB)",
+                            definition: Text(
+                                "Shows the IOB value calculated by the algorithm at a specific time in the past. These values are snapshots and won’t change if insulin is added or removed after the fact."
+                            ),
+                            color: Color.darkerBlue.opacity(0.8),
+                            iconString: "line.diagonal"
+                        )
+
+                        DefinitionRow(
+                            term: "Past Carbs-on-Board (COB)",
+                            definition: Text(
+                                "Shows the COB value calculated by the algorithm at a specific time in the past. These values are snapshots and won’t change if carbs are added or removed after the fact."
+                            ),
+                            color: Color.orange.opacity(0.8),
+                            iconString: "line.diagonal"
+                        )
+                    }.listRowBackground(Color.gray.opacity(0.1))
+                }
+                .scrollContentBackground(.hidden)
+                .navigationBarTitle("Chart Legend", displayMode: .inline)
+                .padding(.trailing, 10)
+                .padding(.bottom, 15)
+
+                Button {
+                    state.isLegendPresented.toggle()
+                } label: {
+                    Text("Got it!")
+                        .frame(maxWidth: .infinity, alignment: .center)
+                }
+                .buttonStyle(.bordered)
+                .padding(.top)
+            }
+            .padding([.horizontal, .bottom])
+            .listSectionSpacing(10)
+            .ignoresSafeArea(edges: .top)
+            .presentationDetents(
+                [.fraction(0.9), .large],
+                selection: $legendSheetDetent
+            )
+        }
+    }
+
+    var legendLinesView: some View {
+        Group {
+            DefinitionRow(
+                term: "IOB (Insulin on Board)",
+                definition: Text(
+                    "Forecasts future glucose readings based on the amount of insulin still active in the body."
+                ),
+                color: .insulin
+            )
+
+            DefinitionRow(
+                term: "ZT (Zero-Temp)",
+                definition: Text(
+                    "Forecasts the worst-case future glucose reading scenario if no carbs are absorbed and insulin delivery is stopped until glucose starts rising."
+                ),
+                color: .zt
+            )
+
+            DefinitionRow(
+                term: "COB (Carbs on Board)",
+                definition: Text(
+                    "Forecasts future glucose reading changes by considering the amount of carbohydrates still being absorbed in the body."
+                ),
+                color: .loopYellow
+            )
+
+            DefinitionRow(
+                term: "UAM (Unannounced Meal)",
+                definition: Text(
+                    "Forecasts future glucose levels and insulin dosing needs for unexpected meals or other causes of glucose reading increases without prior notice."
+                ),
+                color: .uam
+            )
+        }
+    }
+
+    var legendConeOfUncertaintyView: some View {
+        DefinitionRow(
+            term: "Cone of Uncertainty",
+            definition: VStack(alignment: .leading, spacing: 10) {
+                Text(
+                    "For simplicity reasons, oref's various forecast curves are displayed as a \"Cone of Uncertainty\" that depicts a possible, forecasted range of future glucose fluctuation based on the current data and the algothim's result."
+                )
+                Text(
+                    "To modify how the forecast is displayed, go to Settings > Features > User Interface > Forecast Display Type."
+                )
+            },
+            color: Color.blue.opacity(0.5)
+        )
+    }
+}

+ 62 - 144
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -732,7 +732,10 @@ extension Home {
 
                 }.padding(.horizontal, 10).padding(.bottom, UIDevice.adjustPadding(min: nil, max: 10))
                     .overlay(alignment: .bottom) {
-                        bolusProgressBar(progress).padding(.horizontal, 18).offset(y: 48)
+                        // Use a geo-based offset here to position progress bar independent of device size
+                        let offset = geo.size.height * 0.0725
+                        bolusProgressBar(progress).padding(.horizontal, 18)
+                            .offset(y: offset)
                     }.clipShape(RoundedRectangle(cornerRadius: 15))
             }
         }
@@ -898,96 +901,10 @@ extension Home {
                 }
             }
             .sheet(isPresented: $state.isLegendPresented) {
-                legendSheetView()
+                ChartLegendView(state: state)
             }
         }
 
-        @ViewBuilder func legendSheetView() -> some View {
-            NavigationStack {
-                VStack(alignment: .leading, spacing: 16) {
-                    Text(
-                        "The oref algorithm determines insulin dosing based on a number of scenarios that it estimates with different types of forecasts."
-                    )
-                    .font(.subheadline)
-                    .foregroundColor(.secondary)
-
-                    if state.forecastDisplayType == .lines {
-                        legendLinesView()
-                    } else {
-                        legendConeOfUncertaintyView()
-                    }
-
-                    Button {
-                        state.isLegendPresented.toggle()
-                    } label: {
-                        Text("Got it!")
-                            .frame(maxWidth: .infinity, alignment: .center)
-                    }
-                    .buttonStyle(.bordered)
-                    .padding(.top)
-                }
-                .padding()
-                .presentationDetents(
-                    [.fraction(0.9), .large],
-                    selection: $state.legendSheetDetent
-                )
-            }
-        }
-
-        @ViewBuilder func legendLinesView() -> some View {
-            List {
-                DefinitionRow(
-                    term: "IOB (Insulin on Board)",
-                    definition: Text(
-                        "Forecasts future glucose readings based on the amount of insulin still active in the body."
-                    ),
-                    color: .insulin
-                )
-                DefinitionRow(
-                    term: "ZT (Zero-Temp)",
-                    definition: Text(
-                        "Forecasts the worst-case future glucose reading scenario if no carbs are absorbed and insulin delivery is stopped until glucose starts rising."
-                    ),
-                    color: .zt
-                )
-                DefinitionRow(
-                    term: "COB (Carbs on Board)",
-                    definition: Text(
-                        "Forecasts future glucose reading changes by considering the amount of carbohydrates still being absorbed in the body."
-                    ),
-                    color: .loopYellow
-                )
-                DefinitionRow(
-                    term: "UAM (Unannounced Meal)",
-                    definition: Text(
-                        "Forecasts future glucose levels and insulin dosing needs for unexpected meals or other causes of glucose reading increases without prior notice."
-                    ),
-                    color: .uam
-                )
-            }
-            .padding(.trailing, 10)
-            .navigationBarTitle("Legend", displayMode: .inline)
-        }
-
-        @ViewBuilder func legendConeOfUncertaintyView() -> some View {
-            List {
-                DefinitionRow(
-                    term: "Cone of Uncertainty",
-                    definition: VStack {
-                        Text(
-                            "For simplicity reasons, oref's various forecast curves are displayed as a \"Cone of Uncertainty\" that depicts a possible, forecasted range of future glucose fluctuation based on the current data and the algothim's result."
-                        )
-                        Text(
-                            "Note: To modify the forecast display type, go to Trio Settings > Features > User Interface > Forecast Display Type."
-                        )
-                    },
-                    color: Color.blue.opacity(0.5)
-                )
-            }
-            .padding(.trailing, 10)
-            .navigationBarTitle("Legend", displayMode: .inline)
-        }
-
         @ViewBuilder func tabBar() -> some View {
             ZStack(alignment: .bottom) {
                 TabView(selection: $selectedTab) {
@@ -1038,7 +955,7 @@ extension Home {
                             .font(.system(size: 40))
                             .foregroundStyle(Color.tabBar)
                             .padding(.bottom, 1)
-                            .padding(.horizontal, 20)
+                            .padding(.horizontal, 22.5)
                     }
                 )
             }.ignoresSafeArea(.keyboard, edges: .bottom).blur(radius: state.waitForSuggestion ? 8 : 0)
@@ -1059,11 +976,12 @@ extension Home {
         }
 
         // TODO: Consolidate all mmol parsing methods (in TagCloudView, NightscoutManager and HomeRootView) to one central func
-        private func parseReasonConclusion(_ reasonConclusion: String, isMmolL _: Bool) -> String {
+        private func parseReasonConclusion(_ reasonConclusion: String, isMmolL: Bool) -> String {
             let patterns = [
-                "minGuardBG\\s*-?\\d+\\.?\\d*<-?\\d+\\.?\\d*",
-                "Eventual BG\\s*-?\\d+\\.?\\d*\\s*>=\\s*-?\\d+\\.?\\d*",
-                "\\S+\\s+-?\\d+\\.?\\d*\\s*>\\s*\\d+%\\s+of\\s+BG\\s+-?\\d+\\.?\\d*"
+                "minGuardBG\\s*-?\\d+\\.?\\d*<-?\\d+\\.?\\d*", // minGuardBG x<y
+                "Eventual BG\\s*-?\\d+\\.?\\d*\\s*>=\\s*-?\\d+\\.?\\d*", // Eventual BG x >= target
+                "Eventual BG\\s*-?\\d+\\.?\\d*\\s*<\\s*-?\\d+\\.?\\d*",  // Eventual BG x < target
+                "(\\S+)\\s+(-?\\d+\\.?\\d*)\\s*>\\s*(\\d+)%\\s+of\\s+BG\\s+(-?\\d+\\.?\\d*)" // maxDelta x > y% of BG z
             ]
             let pattern = patterns.joined(separator: "|")
             let regex = try! NSRegularExpression(pattern: pattern)
@@ -1086,61 +1004,61 @@ extension Home {
                 guard let range = Range(match.range, in: reasonConclusion) else { continue }
                 let matchedString = String(reasonConclusion[range])
 
-                if matchedString.contains("<") {
-                    // Handle "minGuardBG x<y" pattern
-                    let parts = matchedString.components(separatedBy: "<")
-                    if parts.count == 2,
-                       let firstValue = Double(
-                           parts[0]
-                               .components(separatedBy: CharacterSet(charactersIn: "0123456789.-").inverted).joined()
-                       ),
-                       let secondValue = Double(
-                           parts[1]
-                               .components(separatedBy: CharacterSet(charactersIn: "0123456789.-").inverted).joined()
-                       )
-                    {
-                        let formattedFirstValue = convertToMmolL(String(firstValue))
-                        let formattedSecondValue = convertToMmolL(String(secondValue))
-                        let formattedString = "minGuardBG \(formattedFirstValue)<\(formattedSecondValue)"
-                        updatedConclusion.replaceSubrange(range, with: formattedString)
-                    }
-                } else if matchedString.contains(">=") {
-                    // Handle "Eventual BG x >= target" pattern
-                    let parts = matchedString.components(separatedBy: " >= ")
-                    if parts.count == 2,
-                       let firstValue = Double(
-                           parts[0]
-                               .components(separatedBy: CharacterSet(charactersIn: "0123456789.-").inverted).joined()
-                       ),
-                       let secondValue = Double(
-                           parts[1]
-                               .components(separatedBy: CharacterSet(charactersIn: "0123456789.-").inverted).joined()
-                       )
-                    {
-                        let formattedFirstValue = convertToMmolL(String(firstValue))
-                        let formattedSecondValue = convertToMmolL(String(secondValue))
-                        let formattedString = "Eventual BG \(formattedFirstValue) >= \(formattedSecondValue)"
-                        updatedConclusion.replaceSubrange(range, with: formattedString)
-                    }
-                } else if matchedString.contains(">") {
-                    // Handle "maxDelta 37 > 20% of BG 95" style
-                    let pattern = "(\\S+)\\s+(-?\\d+\\.?\\d*)\\s*>\\s*(\\d+)%\\s+of\\s+BG\\s+(-?\\d+\\.?\\d*)"
-                    let localRegex = try! NSRegularExpression(pattern: pattern)
-                    if let localMatch = localRegex.firstMatch(
+                if isMmolL {
+                    if matchedString.contains("<") && matchedString.contains("Eventual BG") && !matchedString.contains("=") {
+                        // Handle "Eventual BG x < target" pattern
+                        let parts = matchedString.components(separatedBy: "<")
+                        if parts.count == 2 {
+                            let bgPart = parts[0].replacingOccurrences(of: "Eventual BG", with: "").trimmingCharacters(in: .whitespaces)
+                            let targetValue = parts[1].trimmingCharacters(in: .whitespaces)
+                            let formattedBGPart = convertToMmolL(bgPart)
+                            let formattedTargetValue = convertToMmolL(targetValue)
+                            let formattedString = "Eventual BG \(formattedBGPart)<\(formattedTargetValue)"
+                            updatedConclusion.replaceSubrange(range, with: formattedString)
+                        }
+                    } else if matchedString.contains("<") && matchedString.contains("minGuardBG") {
+                        // Handle "minGuardBG x<y" pattern
+                        let parts = matchedString.components(separatedBy: "<")
+                        if parts.count == 2 {
+                            let firstValue = parts[0].trimmingCharacters(in: .whitespaces)
+                            let secondValue = parts[1].trimmingCharacters(in: .whitespaces)
+                            let formattedFirstValue = convertToMmolL(firstValue)
+                            let formattedSecondValue = convertToMmolL(secondValue)
+                            let formattedString = "minGuardBG \(formattedFirstValue)<\(formattedSecondValue)"
+                            updatedConclusion.replaceSubrange(range, with: formattedString)
+                        }
+                    } else if matchedString.contains(">=") {
+                        // Handle "Eventual BG x >= target" pattern
+                        let parts = matchedString.components(separatedBy: " >= ")
+                        if parts.count == 2 {
+                            let firstValue = parts[0].replacingOccurrences(of: "Eventual BG", with: "").trimmingCharacters(in: .whitespaces)
+                            let secondValue = parts[1].trimmingCharacters(in: .whitespaces)
+                            let formattedFirstValue = convertToMmolL(firstValue)
+                            let formattedSecondValue = convertToMmolL(secondValue)
+                            let formattedString = "Eventual BG \(formattedFirstValue) >= \(formattedSecondValue)"
+                            updatedConclusion.replaceSubrange(range, with: formattedString)
+                        }
+                    } else if let localMatch = regex.firstMatch(
                         in: matchedString,
                         range: NSRange(matchedString.startIndex..., in: matchedString)
                     ) {
-                        let metric = String(matchedString[Range(localMatch.range(at: 1), in: matchedString)!])
-                        let firstValue = String(matchedString[Range(localMatch.range(at: 2), in: matchedString)!])
-                        let percentage = String(matchedString[Range(localMatch.range(at: 3), in: matchedString)!])
-                        let bgValue = String(matchedString[Range(localMatch.range(at: 4), in: matchedString)!])
-
-                        let formattedFirstValue = convertToMmolL(firstValue)
-                        let formattedBGValue = convertToMmolL(bgValue)
-
-                        let formattedString = "\(metric) \(formattedFirstValue) > \(percentage)% of BG \(formattedBGValue)"
-                        updatedConclusion.replaceSubrange(range, with: formattedString)
+                        // Handle "maxDelta 37 > 20% of BG 95" style
+                        if match.numberOfRanges == 5 {
+                            let metric = String(matchedString[Range(localMatch.range(at: 1), in: matchedString)!])
+                            let firstValue = String(matchedString[Range(localMatch.range(at: 2), in: matchedString)!])
+                            let percentage = String(matchedString[Range(localMatch.range(at: 3), in: matchedString)!])
+                            let bgValue = String(matchedString[Range(localMatch.range(at: 4), in: matchedString)!])
+
+                            let formattedFirstValue = convertToMmolL(firstValue)
+                            let formattedBGValue = convertToMmolL(bgValue)
+
+                            let formattedString = "\(metric) \(formattedFirstValue) > \(percentage)% of BG \(formattedBGValue)"
+                            updatedConclusion.replaceSubrange(range, with: formattedString)
+                        }
                     }
+                } else {
+                    // When isMmolL is false, ensure the original value is retained without duplication
+                    updatedConclusion.replaceSubrange(range, with: matchedString)
                 }
             }
 

+ 0 - 1
FreeAPS/Sources/Modules/ISFEditor/ISFEditorDataFlow.swift

@@ -27,6 +27,5 @@ enum ISFEditor {
 protocol ISFEditorProvider: Provider {
     var profile: InsulinSensitivities { get }
     func saveProfile(_ profile: InsulinSensitivities)
-    var autosense: Autosens { get }
     var autotune: Autotune? { get }
 }

+ 0 - 6
FreeAPS/Sources/Modules/ISFEditor/ISFEditorProvider.swift

@@ -35,12 +35,6 @@ extension ISFEditor {
             storage.save(profile, as: OpenAPS.Settings.insulinSensitivities)
         }
 
-        var autosense: Autosens {
-            storage.retrieve(OpenAPS.Settings.autosense, as: Autosens.self)
-                ?? Autosens(from: OpenAPS.defaults(for: OpenAPS.Settings.autosense))
-                ?? Autosens(ratio: 1, newisf: nil, timestamp: nil)
-        }
-
         var autotune: Autotune? {
             storage.retrieve(OpenAPS.Settings.autotune, as: Autotune.self)
         }

+ 0 - 34
FreeAPS/Sources/Modules/ISFEditor/ISFEditorStateModel.swift

@@ -10,13 +10,9 @@ extension ISFEditor {
         var items: [Item] = []
         var initialItems: [Item] = []
         var shouldDisplaySaving: Bool = false
-        private(set) var autosensISF: Decimal?
-        private(set) var autosensRatio: Decimal = 0
         var autotune: Autotune?
-        var determinationsFromPersistence: [OrefDetermination] = []
 
         let context = CoreDataStack.shared.newTaskContext()
-        let viewContext = CoreDataStack.shared.persistentContainer.viewContext
 
         let timeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
 
@@ -55,13 +51,6 @@ extension ISFEditor {
             initialItems = items.map { Item(rateIndex: $0.rateIndex, timeIndex: $0.timeIndex) }
 
             autotune = provider.autotune
-
-            if let newISF = provider.autosense.newisf {
-                autosensISF = newISF
-            }
-
-            autosensRatio = provider.autosense.ratio
-            setupDeterminationsArray()
         }
 
         func add() {
@@ -119,29 +108,6 @@ extension ISFEditor {
                 }
             }
         }
-
-        private func setupDeterminationsArray() {
-            Task {
-                let ids = await determinationStorage.fetchLastDeterminationObjectID(
-                    predicate: NSPredicate.enactedDetermination
-                )
-                await updateDeterminationsArray(with: ids)
-            }
-        }
-
-        @MainActor private func updateDeterminationsArray(with IDs: [NSManagedObjectID]) {
-            do {
-                let objects = try IDs.compactMap { id in
-                    try viewContext.existingObject(with: id) as? OrefDetermination
-                }
-                determinationsFromPersistence = objects
-
-            } catch {
-                debugPrint(
-                    "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the glucose array: \(error.localizedDescription)"
-                )
-            }
-        }
     }
 }
 

+ 0 - 45
FreeAPS/Sources/Modules/ISFEditor/View/ISFEditorRootView.swift

@@ -18,13 +18,6 @@ extension ISFEditor {
             return formatter
         }
 
-        private var rateFormatter: NumberFormatter {
-            let formatter = NumberFormatter()
-            formatter.numberStyle = .decimal
-            formatter.maximumFractionDigits = 2
-            return formatter
-        }
-
         var saveButton: some View {
             ZStack {
                 let shouldDisableButton = state.items.isEmpty || !state.hasChanges
@@ -82,44 +75,6 @@ extension ISFEditor {
                         }
                     }.listRowBackground(Color.chart)
                 }
-                if let newISF = state.autosensISF {
-                    Section(
-                        header: !state.settingsManager.preferences
-                            .useNewFormula ? Text("Autosens") : Text("Dynamic Sensitivity")
-                    ) {
-                        let dynamicRatio = state.determinationsFromPersistence.first?.sensitivityRatio
-                        let dynamicISF = state.determinationsFromPersistence.first?.insulinSensitivity
-                        HStack {
-                            Text("Sensitivity Ratio")
-                            Spacer()
-                            Text(
-                                rateFormatter
-                                    .string(from: (
-                                        (
-                                            !state.settingsManager.preferences.useNewFormula ? state
-                                                .autosensRatio as NSDecimalNumber : dynamicRatio
-                                        ) ?? 1
-                                    ) as NSNumber) ?? "1"
-                            )
-                        }
-                        HStack {
-                            Text("Calculated Sensitivity")
-                            Spacer()
-                            if state.units == .mgdL {
-                                Text(
-                                    !state.settingsManager.preferences
-                                        .useNewFormula ? newISF.description : (dynamicISF ?? 0).description
-                                )
-                            } else {
-                                Text((
-                                    !state.settingsManager.preferences
-                                        .useNewFormula ? newISF.formattedAsMmolL : dynamicISF?.decimalValue.formattedAsMmolL
-                                ) ?? "0")
-                            }
-                            Text(state.units.rawValue + "/U").foregroundColor(.secondary)
-                        }
-                    }.listRowBackground(Color.chart)
-                }
 
                 if !state.canAdd {
                     Section {

+ 0 - 2
FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift

@@ -32,7 +32,6 @@ extension NightscoutConfig {
         @Published var dia: Decimal = 6
         @Published var maxBasal: Decimal = 2
         @Published var maxBolus: Decimal = 10
-        @Published var allowAnnouncements: Bool = false
         @Published var isConnectedToNS: Bool = false
 
         @Published var isImportResultReviewPresented: Bool = false
@@ -57,7 +56,6 @@ extension NightscoutConfig {
             maxBolus = settingsManager.pumpSettings.maxBolus
             changeUploadGlucose = (cgmManager.cgmGlucoseSourceType != CGMType.plugin)
 
-            subscribeSetting(\.allowAnnouncements, on: $allowAnnouncements) { allowAnnouncements = $0 }
             subscribeSetting(\.isUploadEnabled, on: $isUploadEnabled) { isUploadEnabled = $0 }
             subscribeSetting(\.isDownloadEnabled, on: $isDownloadEnabled) { isDownloadEnabled = $0 }
             subscribeSetting(\.useLocalGlucoseSource, on: $useLocalSource) { useLocalSource = $0 }

+ 1 - 1
FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutConfigRootView.swift

@@ -41,7 +41,7 @@ extension NightscoutConfig {
                                 }
                             })
                             NavigationLink("Upload", destination: NightscoutUploadView(state: state))
-                            NavigationLink("Fetch & Remote Control", destination: NightscoutFetchView(state: state))
+                            NavigationLink("Fetch", destination: NightscoutFetchView(state: state))
                         }
                     ).listRowBackground(Color.chart)
 

+ 2 - 35
FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutFetchView.swift

@@ -37,41 +37,8 @@ struct NightscoutFetchView: View {
                         "The Fetch Treatments toggle enables fetching of carbs and temp targets entered in Careportal or by another uploading device than Trio from Nightscout."
                     )
                 },
-                headerText: "Remote & Fetch Capabilities"
+                headerText: "Fetch NS Care Portal Data"
             )
-
-            if state.isDownloadEnabled {
-                SettingInputSection(
-                    decimalValue: $decimalPlaceholder,
-                    booleanValue: $state.allowAnnouncements,
-                    shouldDisplayHint: $shouldDisplayHint,
-                    selectedVerboseHint: Binding(
-                        get: { selectedVerboseHint },
-                        set: {
-                            selectedVerboseHint = $0.map { AnyView($0) }
-                            hintLabel = "Allow Remote Control of Trio"
-                        }
-                    ),
-                    units: state.units,
-                    type: .boolean,
-                    label: "Allow Remote Control of Trio",
-                    miniHint: "Enables selected remote control capabilities via Nightscout.",
-                    verboseHint: VStack(alignment: .leading, spacing: 10) {
-                        Text("Default: OFF").bold()
-                        Text("When enabled you allow the following remote functions through announcements from Nightscout:")
-                        VStack(alignment: .leading) {
-                            Text("• Suspend/Resume Pump")
-                            Text("• Opening/Closing Loop")
-                            Text("• Set Temp Basal")
-                            Text("• Enact Bolus")
-                        }
-                    }
-                )
-            } else {
-                Section {
-                    Text("'Allow Fetching from Nightscout' must be enabled to allow for Trio Remote Control.")
-                }.listRowBackground(Color.tabBar)
-            }
         }
         .listSectionSpacing(sectionSpacing)
         .sheet(isPresented: $shouldDisplayHint) {
@@ -83,7 +50,7 @@ struct NightscoutFetchView: View {
                 sheetTitle: "Help"
             )
         }
-        .navigationTitle("Fetch & Remote")
+        .navigationTitle("Fetch")
         .navigationBarTitleDisplayMode(.automatic)
         .scrollContentBackground(.hidden)
         .background(appState.trioBackgroundColor(for: colorScheme))

+ 0 - 4
FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift

@@ -266,10 +266,6 @@ extension Settings {
 //                                .navigationLink(to: .configEditor(file: OpenAPS.Settings.profile), from: self)
 //                            //                            Text("Carbs")
 //                            //                                .navigationLink(to: .configEditor(file: OpenAPS.Monitor.carbHistory), from: self)
-//                            //                            Text("Announcements")
-//                            //                                .navigationLink(to: .configEditor(file: OpenAPS.FreeAPS.announcements), from: self)
-//                            //                            Text("Enacted announcements")
-//                            //                                .navigationLink(to: .configEditor(file: OpenAPS.FreeAPS.announcementsEnacted), from: self)
 //                            Text("Autotune")
 //                                .navigationLink(to: .configEditor(file: OpenAPS.Settings.autotune), from: self)
 //                        }

+ 24 - 0
FreeAPS/Sources/Modules/Treatments/TreatmentsStateModel.swift

@@ -126,6 +126,9 @@ extension Treatments {
 
         typealias PumpEvent = PumpEventStored.EventType
 
+        var isBolusInProgress: Bool = false
+        private var bolusProgressCancellable: AnyCancellable?
+
         func unsubscribe() {
             subscriptions.forEach { $0.cancel() }
             subscriptions.removeAll()
@@ -145,6 +148,7 @@ extension Treatments {
             registerHandlers()
             registerSubscribers()
             setupBolusStateConcurrently()
+            subscribeToBolusProgress()
         }
 
         deinit {
@@ -155,6 +159,8 @@ extension Treatments {
             // Cancel Combine subscriptions
             unsubscribe()
 
+            bolusProgressCancellable?.cancel()
+
             debug(.bolusState, "Bolus.StateModel deinitialized")
         }
 
@@ -191,6 +197,24 @@ extension Treatments {
             }
         }
 
+        /// Observes changes to the `bolusProgress` published by the `apsManager` to update the `isBolusInProgress` property in real time.
+        ///
+        /// - Important:
+        ///   - `apsManager.bolusProgress` is a `CurrentValueSubject<Decimal?, Never>`.
+        ///   - When a bolus starts, this subject emits `0` (or a fraction like `0.1, 0.5, etc.`).
+        ///   - When the bolus finishes, the subject is typically set to `nil`.
+        ///   - This treats ANY non-nil value as “bolus in progress.”
+        ///
+        private func subscribeToBolusProgress() {
+            bolusProgressCancellable = apsManager.bolusProgress
+                .receive(on: DispatchQueue.main)
+                .sink { [weak self] progressValue in
+                    guard let self = self else { return }
+                    // If progressValue is non-nil, a bolus is in progress.
+                    self.isBolusInProgress = (progressValue != nil)
+                }
+        }
+
         // MARK: - Basal
 
         private enum SettingType {

+ 31 - 13
FreeAPS/Sources/Modules/Treatments/View/TreatmentsRootView.swift

@@ -365,28 +365,38 @@ extension Treatments {
         }
 
         var treatmentButton: some View {
-            Button {
+            var treatmentButtonBackground = Color(.systemBlue)
+            if limitExceeded {
+                treatmentButtonBackground = Color(.systemRed)
+            } else if disableTaskButton {
+                treatmentButtonBackground = Color(.systemGray)
+            }
+
+            return Button {
                 state.invokeTreatmentsTask()
             } label: {
-                taskButtonLabel
-                    .font(.headline)
-                    .foregroundStyle(Color.white)
-                    .frame(maxWidth: .infinity, alignment: .center)
-                    .frame(height: 35)
+                HStack {
+                    if state.isBolusInProgress && state
+                        .amount > 0 && !state.externalInsulin && (state.carbs == 0 || state.fat == 0 || state.protein == 0)
+                    {
+                        ProgressView()
+                    }
+                    taskButtonLabel
+                }
+                .font(.headline)
+                .foregroundStyle(Color.white)
+                .frame(maxWidth: .infinity, alignment: .center)
+                .frame(height: 35)
             }
             .disabled(disableTaskButton)
-            .listRowBackground(
-                limitExceeded ? Color(.systemRed) :
-                    disableTaskButton ? Color(.systemGray) :
-                    Color(.systemBlue)
-            )
+            .listRowBackground(treatmentButtonBackground)
             .shadow(radius: 3)
             .clipShape(RoundedRectangle(cornerRadius: 8))
         }
 
         private var taskButtonLabel: some View {
             if pumpBolusLimitExceeded {
-                return Text("Max Bolus of \(state.maxBolus.description) U Exceeded")
+                return Text("Max Bolus of \(state.maxBolus.description) U E== 0xceeded")
             } else if externalBolusLimitExceeded {
                 return Text("Max External Bolus of \(state.maxExternal.description) U Exceeded")
             } else if carbLimitExceeded {
@@ -402,6 +412,10 @@ extension Treatments {
             let hasFatOrProtein = state.fat > 0 || state.protein > 0
             let bolusString = state.externalInsulin ? "External Insulin" : "Enact Bolus"
 
+            if state.isBolusInProgress && hasInsulin && !state.externalInsulin && (!hasCarbs || !hasFatOrProtein) {
+                return Text("Bolus In Progress...")
+            }
+
             switch (hasInsulin, hasCarbs, hasFatOrProtein) {
             case (true, true, true):
                 return Text("Log Meal and \(bolusString)")
@@ -447,7 +461,11 @@ extension Treatments {
         }
 
         private var disableTaskButton: Bool {
-            state.addButtonPressed || limitExceeded
+            (
+                state.isBolusInProgress && state
+                    .amount > 0 && !state.externalInsulin && (state.carbs == 0 || state.fat == 0 || state.protein == 0)
+            ) || state
+                .addButtonPressed || limitExceeded
         }
     }
 

+ 0 - 35
FreeAPS/Sources/Services/Network/Nightscout/NightscoutAPI.swift

@@ -286,41 +286,6 @@ extension NightscoutAPI {
         }
     }
 
-    func fetchAnnouncement(sinceDate: Date? = nil) -> AnyPublisher<[Announcement], Swift.Error> {
-        var components = URLComponents()
-        components.scheme = url.scheme
-        components.host = url.host
-        components.port = url.port
-        components.path = Config.treatmentsPath
-        components.queryItems = [
-            URLQueryItem(name: "find[eventType]", value: "Announcement"),
-            URLQueryItem(
-                name: "find[enteredBy]",
-                value: Announcement.remote.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
-            )
-        ]
-        if let date = sinceDate {
-            let dateItem = URLQueryItem(
-                name: "find[created_at][$gte]",
-                value: Formatter.iso8601withFractionalSeconds.string(from: date)
-            )
-            components.queryItems?.append(dateItem)
-        }
-
-        var request = URLRequest(url: components.url!)
-        request.allowsConstrainedNetworkAccess = false
-        request.timeoutInterval = Config.timeout
-
-        if let secret = secret {
-            request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
-        }
-
-        return service.run(request)
-            .retry(Config.retryCount)
-            .decode(type: [Announcement].self, decoder: JSONCoding.decoder)
-            .eraseToAnyPublisher()
-    }
-
     func uploadTreatments(_ treatments: [NightscoutTreatment]) async throws {
         var components = URLComponents()
         components.scheme = url.scheme

+ 42 - 72
FreeAPS/Sources/Services/Network/Nightscout/NightscoutManager.swift

@@ -9,7 +9,6 @@ protocol NightscoutManager: GlucoseSource {
     func fetchGlucose(since date: Date) async -> [BloodGlucose]
     func fetchCarbs() async -> [CarbsEntry]
     func fetchTempTargets() async -> [TempTarget]
-    func fetchAnnouncements() -> AnyPublisher<[Announcement], Never>
     func deleteCarbs(withID id: String) async
     func deleteInsulin(withID id: String) async
     func deleteManualGlucose(withID id: String) async
@@ -35,7 +34,6 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     @Injected() private var carbsStorage: CarbsStorage!
     @Injected() private var pumpHistoryStorage: PumpHistoryStorage!
     @Injected() private var storage: FileStorage!
-    @Injected() private var announcementsStorage: AnnouncementsStorage!
     @Injected() private var settingsManager: SettingsManager!
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var reachabilityManager: ReachabilityManager!
@@ -319,23 +317,9 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         }
     }
 
-    func fetchAnnouncements() -> AnyPublisher<[Announcement], Never> {
-        guard let nightscout = nightscoutAPI, isNetworkReachable, isDownloadEnabled else {
-            return Just([]).eraseToAnyPublisher()
-        }
-
-        let since = announcementsStorage.syncDate()
-        return nightscout.fetchAnnouncement(sinceDate: since)
-            .replaceError(with: [])
-            .eraseToAnyPublisher()
-    }
-
     func deleteCarbs(withID id: String) async {
         guard let nightscout = nightscoutAPI, isUploadEnabled else { return }
 
-        // TODO: - healthkit rewrite, deletion of FPUs
-//        healthkitManager.deleteCarbs(syncID: arg1, fpuID: arg2)
-
         do {
             try await nightscout.deleteCarbs(withId: id)
             debug(.nightscout, "Carbs deleted")
@@ -445,7 +429,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         var modifiedSuggestedDetermination = fetchedSuggestedDetermination
         if var suggestion = fetchedSuggestedDetermination {
             suggestion.timestamp = suggestion.deliverAt
-            
+
             if settingsManager.settings.units == .mmolL {
                 suggestion.reason = parseReasonGlucoseValuesToMmolL(suggestion.reason)
                 // TODO: verify that these parsings are needed for 3rd party apps, e.g., LoopFollow
@@ -1152,17 +1136,18 @@ extension BaseNightscoutManager {
      - Glucose tags handled: `ISF:`, `Target:`, `minPredBG`, `minGuardBG`, `IOBpredBG`, `COBpredBG`, `UAMpredBG`, `Dev:`, `maxDelta`, `BGI`.
      */
 
-     //TODO: Consolidate all mmol parsing methods (in TagCloudView, NightscoutManager and HomeRootView) to one central func
+    // TODO: Consolidate all mmol parsing methods (in TagCloudView, NightscoutManager and HomeRootView) to one central func
     func parseReasonGlucoseValuesToMmolL(_ reason: String) -> String {
         let patterns = [
-            "ISF:\\s*-?\\d+\\.?\\d*→-?\\d+\\.?\\d*",
-            "Dev:\\s*-?\\d+\\.?\\d*",
-            "BGI:\\s*-?\\d+\\.?\\d*",
-            "Target:\\s*-?\\d+\\.?\\d*",
-            "(?:minPredBG|minGuardBG|IOBpredBG|COBpredBG|UAMpredBG)\\s+-?\\d+\\.?\\d*(?:<-?\\d+\\.?\\d*)?",
-            "minGuardBG\\s+-?\\d+\\.?\\d*<-?\\d+\\.?\\d*",
-            "Eventual BG\\s+-?\\d+\\.?\\d*\\s*>=\\s*-?\\d+\\.?\\d*",
-            "\\S+\\s+\\d+\\s*>\\s*\\d+%\\s+of\\s+BG\\s+\\d+"
+            "ISF:\\s*-?\\d+\\.?\\d*→-?\\d+\\.?\\d*", // ISF with arrow
+            "Dev:\\s*-?\\d+\\.?\\d*", // Dev pattern
+            "BGI:\\s*-?\\d+\\.?\\d*", // BGI pattern
+            "Target:\\s*-?\\d+\\.?\\d*", // Target pattern
+            "(?:minPredBG|minGuardBG|IOBpredBG|COBpredBG|UAMpredBG)\\s+-?\\d+\\.?\\d*(?:<-?\\d+\\.?\\d*)?", // minPredBG, etc.
+            "minGuardBG\\s+-?\\d+\\.?\\d*<-?\\d+\\.?\\d*", // minGuardBG x<y
+            "Eventual BG\\s+-?\\d+\\.?\\d*\\s*>=\\s*-?\\d+\\.?\\d*", // Eventual BG x >= target
+            "Eventual BG\\s+-?\\d+\\.?\\d*\\s*<\\s*-?\\d+\\.?\\d*", // Eventual BG x < target
+            "\\S+\\s+\\d+\\s*>\\s*\\d+%\\s+of\\s+BG\\s+\\d+" // maxDelta x > y% of BG z
         ]
         let pattern = patterns.joined(separator: "|")
         let regex = try! NSRegularExpression(pattern: pattern)
@@ -1183,7 +1168,7 @@ extension BaseNightscoutManager {
             let glucoseValueString = String(reason[range])
 
             if glucoseValueString.contains("→") {
-                // -- Handle ISF: X→Y
+                // Handle ISF: X→Y
                 let values = glucoseValueString.components(separatedBy: "→")
                 let firstNumber = values[0].components(separatedBy: ":")[1].trimmingCharacters(in: .whitespaces)
                 let secondNumber = values[1].trimmingCharacters(in: .whitespaces)
@@ -1192,58 +1177,44 @@ extension BaseNightscoutManager {
                 let formattedString = "ISF: \(firstValue)→\(secondValue)"
                 updatedReason.replaceSubrange(range, with: formattedString)
 
+            } else if glucoseValueString.contains("Eventual BG"), glucoseValueString.contains("<") {
+                // Handle Eventual BG XX < target
+                let parts = glucoseValueString.components(separatedBy: "<")
+                if parts.count == 2 {
+                    let bgPart = parts[0].replacingOccurrences(of: "Eventual BG", with: "").trimmingCharacters(in: .whitespaces)
+                    let targetValue = parts[1].trimmingCharacters(in: .whitespaces)
+                    let formattedBGPart = convertToMmolL(bgPart)
+                    let formattedTargetValue = convertToMmolL(targetValue)
+                    let formattedString = "Eventual BG \(formattedBGPart)<\(formattedTargetValue)"
+                    updatedReason.replaceSubrange(range, with: formattedString)
+                }
+
             } else if glucoseValueString.contains("<") {
-                // -- Handle minGuardBG (or minPredBG, etc.) x < y
-                let components = glucoseValueString
-                    .split(whereSeparator: { "<".contains($0) ||
-                            CharacterSet.whitespaces.contains($0.unicodeScalars.first!) })
-                    .filter { !$0.isEmpty }
-
-                if components.count >= 3 {
-                    let firstValue = convertToMmolL(String(components[1]))
-                    let secondValue = convertToMmolL(String(components[2]))
-                    let formattedString = "\(components[0]) \(firstValue)<\(secondValue)"
+                // Handle minGuardBG (or minPredBG, etc.) x < y
+                let parts = glucoseValueString.components(separatedBy: "<")
+                if parts.count == 2 {
+                    let firstValue = parts[0].trimmingCharacters(in: .whitespaces)
+                    let secondValue = parts[1].trimmingCharacters(in: .whitespaces)
+                    let formattedFirstValue = convertToMmolL(firstValue)
+                    let formattedSecondValue = convertToMmolL(secondValue)
+                    let formattedString = "minGuardBG \(formattedFirstValue)<\(formattedSecondValue)"
                     updatedReason.replaceSubrange(range, with: formattedString)
                 }
 
             } else if glucoseValueString.contains(">=") {
-                // -- Handle "Eventual BG X >= Y"
-                let components = glucoseValueString
-                    .split(whereSeparator: { " >= ".contains($0) ||
-                            CharacterSet.whitespaces.contains($0.unicodeScalars.first!) })
-                    .filter { !$0.isEmpty }
-
-                if components.count == 4 {
-                    let firstValue = convertToMmolL(String(components[2]))
-                    let secondValue = convertToMmolL(String(components[3]))
-                    let formattedString = "\(components[0]) \(components[1]) \(firstValue) >= \(secondValue)"
+                // Handle "Eventual BG X >= Y"
+                let parts = glucoseValueString.components(separatedBy: " >= ")
+                if parts.count == 2 {
+                    let firstValue = parts[0].replacingOccurrences(of: "Eventual BG", with: "").trimmingCharacters(in: .whitespaces)
+                    let secondValue = parts[1].trimmingCharacters(in: .whitespaces)
+                    let formattedFirstValue = convertToMmolL(firstValue)
+                    let formattedSecondValue = convertToMmolL(secondValue)
+                    let formattedString = "Eventual BG \(formattedFirstValue) >= \(formattedSecondValue)"
                     updatedReason.replaceSubrange(range, with: formattedString)
                 }
 
-            } else if glucoseValueString.starts(with: "Dev:") {
-                // -- Handle Dev
-                let value = glucoseValueString.components(separatedBy: ":")[1].trimmingCharacters(in: .whitespaces)
-                let formattedValue = convertToMmolL(value)
-                let formattedString = "Dev: \(formattedValue)"
-                updatedReason.replaceSubrange(range, with: formattedString)
-
-            } else if glucoseValueString.starts(with: "BGI:") {
-                // -- Handle BGI
-                let value = glucoseValueString.components(separatedBy: ":")[1].trimmingCharacters(in: .whitespaces)
-                let formattedValue = convertToMmolL(value)
-                let formattedString = "BGI: \(formattedValue)"
-                updatedReason.replaceSubrange(range, with: formattedString)
-
-            } else if glucoseValueString.starts(with: "Target:") {
-                // -- Handle Target
-                let value = glucoseValueString.components(separatedBy: ":")[1].trimmingCharacters(in: .whitespaces)
-                let formattedValue = convertToMmolL(value)
-                let formattedString = "Target: \(formattedValue)"
-                updatedReason.replaceSubrange(range, with: formattedString)
-
             } else if glucoseValueString.contains(">"), glucoseValueString.contains("BG") {
-                // -- Handle "maxDelta 37 > 20% of BG 95" style
-                // Run a local regex that picks out the two BG values
+                // Handle "maxDelta 37 > 20% of BG 95" style
                 let localPattern = "(\\d+) > (\\d+)% of BG (\\d+)"
                 let localRegex = try! NSRegularExpression(pattern: localPattern)
                 let localMatches = localRegex.matches(
@@ -1258,7 +1229,6 @@ extension BaseNightscoutManager {
                     let firstValue = convertToMmolL(String(glucoseValueString[range1]))
                     let thirdValue = convertToMmolL(String(glucoseValueString[range3]))
 
-                    // e.g. "37 > 20% of BG 95" → "2.1 > 20% of BG 5.3"
                     let oldSnippet =
                         "\(glucoseValueString[range1]) > \(glucoseValueString[range2])% of BG \(glucoseValueString[range3])"
                     let newSnippet = "\(firstValue) > \(glucoseValueString[range2])% of BG \(thirdValue)"
@@ -1268,7 +1238,7 @@ extension BaseNightscoutManager {
                 }
 
             } else {
-                // -- Handle everything else, e.g. "minPredBG 39" etc.
+                // Handle everything else, e.g., "minPredBG 39", "COB 29", etc.
                 let parts = glucoseValueString.components(separatedBy: .whitespaces)
                 if parts.count >= 2 {
                     let metric = parts[0]

+ 26 - 1
FreeAPS/Sources/Views/DefinitionRow.swift

@@ -6,12 +6,37 @@ struct DefinitionRow<DefinitionView: View>: View {
     var definition: DefinitionView
     var color: Color?
     var fontSize: Font?
+    var iconString: String?
+    var shouldRotateIcon: Bool?
+
+    init(
+        term: String,
+        definition: DefinitionView,
+        color: Color? = nil,
+        fontSize: Font? = nil,
+        iconString: String? = nil,
+        shouldRotateIcon: Bool = false
+    ) {
+        self.term = term
+        self.definition = definition
+        self.color = color
+        self.fontSize = fontSize
+        self.iconString = iconString
+        self.shouldRotateIcon = shouldRotateIcon
+    }
 
     var body: some View {
         VStack(alignment: .leading) {
             HStack {
                 if let color = color {
-                    Image(systemName: "circle.fill").foregroundStyle(color)
+                    if let iconString = iconString {
+                        Image(systemName: iconString)
+                            .foregroundStyle(color)
+                            .rotationEffect(shouldRotateIcon == true ? .degrees(180) : .degrees(0))
+                    } else {
+                        Image(systemName: "circle.fill")
+                            .foregroundStyle(color)
+                    }
                 }
                 Text(term).font(fontSize ?? .subheadline).fontWeight(.semibold)
             }.padding(.bottom, 5)

+ 2 - 18
FreeAPS/Sources/Views/SettingInputHintView.swift

@@ -7,24 +7,6 @@ struct SettingInputHintView<HintView: View>: View {
     var hintText: HintView
     var sheetTitle: String
 
-    @Environment(\.colorScheme) private var colorScheme
-    private var color: LinearGradient {
-        colorScheme == .dark ? LinearGradient(
-            gradient: Gradient(colors: [
-                Color.bgDarkBlue,
-                Color.bgDarkerDarkBlue
-            ]),
-            startPoint: .top,
-            endPoint: .bottom
-        )
-            :
-            LinearGradient(
-                gradient: Gradient(colors: [Color.gray.opacity(0.1)]),
-                startPoint: .top,
-                endPoint: .bottom
-            )
-    }
-
     var body: some View {
         NavigationStack {
             List {
@@ -33,7 +15,9 @@ struct SettingInputHintView<HintView: View>: View {
                     definition: hintText,
                     fontSize: .body
                 )
+                .listRowBackground(Color.gray.opacity(0.1))
             }
+            .scrollContentBackground(.hidden)
             .navigationBarTitle(sheetTitle, displayMode: .inline)
 
             Spacer()

+ 1 - 1
FreeAPS/Sources/Views/TagCloudView.swift

@@ -104,7 +104,7 @@ struct TagCloudView: View {
      - Glucose tags handled: `ISF:`, `Target:`, `minPredBG`, `minGuardBG`, `IOBpredBG`, `COBpredBG`, `UAMpredBG`, `Dev:`, `maxDelta`, `BGI`.
      */
 
-     //TODO: Consolidate all mmol parsing methods (in TagCloudView, NightscoutManager and HomeRootView) to one central func
+    // TODO: Consolidate all mmol parsing methods (in TagCloudView, NightscoutManager and HomeRootView) to one central func
     private func formatGlucoseTags(_ tag: String, isMmolL: Bool) -> String {
         let patterns = [
             "ISF:\\s*-?\\d+\\.?\\d*→-?\\d+\\.?\\d*",

+ 0 - 1
LiveActivity/Views/WidgetItems/LiveActivityIOBLabelView.swift

@@ -16,7 +16,6 @@ struct LiveActivityIOBLabelView: View {
         let formatter = NumberFormatter()
         formatter.numberStyle = .decimal
         formatter.maximumFractionDigits = 1
-        formatter.decimalSeparator = "."
         return formatter
     }
 

+ 2 - 2
Trio.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -1,5 +1,5 @@
 {
-  "originHash" : "59ac7eba66375d6eb406e758cb0b9964f4b3b0ae45c5665596f00384c32262b9",
+  "originHash" : "52d77fc35af7fe71614051dee0b291e2a0d38522eac7ae4d37d2442e81c7530c",
   "pins" : [
     {
       "identity" : "cryptoswift",
@@ -49,7 +49,7 @@
     {
       "identity" : "swiftcharts",
       "kind" : "remoteSourceControl",
-      "location" : "https://github.com/ivanschuetz/SwiftCharts",
+      "location" : "https://github.com/ivanschuetz/SwiftCharts.git",
       "state" : {
         "branch" : "master",
         "revision" : "c354c1945bb35a1f01b665b22474f6db28cba4a2"