Bläddra i källkod

Pull in latest Settings branch

polscm32 1 år sedan
förälder
incheckning
9f472de77d
40 ändrade filer med 395 tillägg och 179 borttagningar
  1. 3 2
      FreeAPS.xcodeproj/project.pbxproj
  2. 9 0
      FreeAPS/Sources/APS/APSManager.swift
  3. 1 1
      FreeAPS/Sources/APS/CGM/CGMType.swift
  4. 1 1
      FreeAPS/Sources/Localizations/Main/ar.lproj/Localizable.strings
  5. 1 1
      FreeAPS/Sources/Localizations/Main/ca.lproj/Localizable.strings
  6. 1 1
      FreeAPS/Sources/Localizations/Main/da.lproj/Localizable.strings
  7. 1 1
      FreeAPS/Sources/Localizations/Main/de.lproj/Localizable.strings
  8. 1 1
      FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings
  9. 1 1
      FreeAPS/Sources/Localizations/Main/es.lproj/Localizable.strings
  10. 1 1
      FreeAPS/Sources/Localizations/Main/fi.lproj/Localizable.strings
  11. 1 1
      FreeAPS/Sources/Localizations/Main/fr.lproj/Localizable.strings
  12. 1 1
      FreeAPS/Sources/Localizations/Main/he.lproj/Localizable.strings
  13. 1 1
      FreeAPS/Sources/Localizations/Main/hu.lproj/Localizable.strings
  14. 1 1
      FreeAPS/Sources/Localizations/Main/it.lproj/Localizable.strings
  15. 1 1
      FreeAPS/Sources/Localizations/Main/nb.lproj/Localizable.strings
  16. 1 1
      FreeAPS/Sources/Localizations/Main/nl.lproj/Localizable.strings
  17. 1 1
      FreeAPS/Sources/Localizations/Main/pl.lproj/Localizable.strings
  18. 1 1
      FreeAPS/Sources/Localizations/Main/pt-BR.lproj/Localizable.strings
  19. 1 1
      FreeAPS/Sources/Localizations/Main/pt-PT.lproj/Localizable.strings
  20. 1 1
      FreeAPS/Sources/Localizations/Main/ru.lproj/Localizable.strings
  21. 1 1
      FreeAPS/Sources/Localizations/Main/sk.lproj/Localizable.strings
  22. 1 1
      FreeAPS/Sources/Localizations/Main/sv.lproj/Localizable.strings
  23. 1 1
      FreeAPS/Sources/Localizations/Main/tr.lproj/Localizable.strings
  24. 1 1
      FreeAPS/Sources/Localizations/Main/uk.lproj/Localizable.strings
  25. 1 1
      FreeAPS/Sources/Localizations/Main/vi.lproj/Localizable.strings
  26. 1 1
      FreeAPS/Sources/Localizations/Main/zh-Hans.lproj/Localizable.strings
  27. 2 2
      FreeAPS/Sources/Models/BGTargets.swift
  28. 10 10
      FreeAPS/Sources/Models/Determination.swift
  29. 3 3
      FreeAPS/Sources/Models/InsulinSensitivities.swift
  30. 1 1
      FreeAPS/Sources/Modules/GeneralSettings/View/UnitsLimitsSettingsRootView.swift
  31. 5 5
      FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift
  32. 1 8
      FreeAPS/Sources/Modules/Home/View/HomeRootView.swift
  33. 7 1
      FreeAPS/Sources/Modules/ISFEditor/ISFEditorProvider.swift
  34. 0 23
      FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutConfigRootView.swift
  35. 12 1
      FreeAPS/Sources/Modules/OverrideConfig/View/OverrideRootView.swift
  36. 7 0
      FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift
  37. 8 1
      FreeAPS/Sources/Modules/TargetsEditor/TargetsEditorProvider.swift
  38. 109 2
      FreeAPS/Sources/Services/Network/NightscoutManager.swift
  39. 112 1
      FreeAPS/Sources/Services/Storage/FileStorage.swift
  40. 82 95
      FreeAPS/Sources/Views/TagCloudView.swift

+ 3 - 2
FreeAPS.xcodeproj/project.pbxproj

@@ -17,12 +17,12 @@
 		110AEDEC2C51A0AE00615CC9 /* ShortcutsConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 110AEDE72C51A0AE00615CC9 /* ShortcutsConfigDataFlow.swift */; };
 		110AEDED2C51A0AE00615CC9 /* ShortcutsConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 110AEDE82C51A0AE00615CC9 /* ShortcutsConfigProvider.swift */; };
 		110AEDEE2C51A0AE00615CC9 /* ShortcutsConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 110AEDE92C51A0AE00615CC9 /* ShortcutsConfigStateModel.swift */; };
+		17A9D0899046B45E87834820 /* CarbRatioEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C8D5F457B5AFF763F8CF3DF /* CarbRatioEditorProvider.swift */; };
 		118DF76A2C5ECBC60067FEB7 /* ApplyOverridePresetIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 118DF7642C5ECBC60067FEB7 /* ApplyOverridePresetIntent.swift */; };
 		118DF76B2C5ECBC60067FEB7 /* CancelOverrideIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 118DF7652C5ECBC60067FEB7 /* CancelOverrideIntent.swift */; };
 		118DF76C2C5ECBC60067FEB7 /* ListOverridePresetIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 118DF7662C5ECBC60067FEB7 /* ListOverridePresetIntent.swift */; };
 		118DF76D2C5ECBC60067FEB7 /* OverridePresetEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 118DF7672C5ECBC60067FEB7 /* OverridePresetEntity.swift */; };
 		118DF76E2C5ECBC60067FEB7 /* OverridePresetsIntentRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 118DF7682C5ECBC60067FEB7 /* OverridePresetsIntentRequest.swift */; };
-		17A9D0899046B45E87834820 /* CarbRatioEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C8D5F457B5AFF763F8CF3DF /* CarbRatioEditorProvider.swift */; };
 		19012CDC291D2CB900FB8210 /* LoopStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19012CDB291D2CB900FB8210 /* LoopStats.swift */; };
 		190EBCC429FF136900BA767D /* UserInterfaceSettingsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 190EBCC329FF136900BA767D /* UserInterfaceSettingsDataFlow.swift */; };
 		190EBCC629FF138000BA767D /* UserInterfaceSettingsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 190EBCC529FF138000BA767D /* UserInterfaceSettingsProvider.swift */; };
@@ -607,6 +607,7 @@
 		118DF7662C5ECBC60067FEB7 /* ListOverridePresetIntent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListOverridePresetIntent.swift; sourceTree = "<group>"; };
 		118DF7672C5ECBC60067FEB7 /* OverridePresetEntity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverridePresetEntity.swift; sourceTree = "<group>"; };
 		118DF7682C5ECBC60067FEB7 /* OverridePresetsIntentRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverridePresetsIntentRequest.swift; sourceTree = "<group>"; };
+		12204445D7632AF09264A979 /* PreferencesEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PreferencesEditorDataFlow.swift; sourceTree = "<group>"; };
 		19012CDB291D2CB900FB8210 /* LoopStats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopStats.swift; sourceTree = "<group>"; };
 		190EBCC329FF136900BA767D /* UserInterfaceSettingsDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserInterfaceSettingsDataFlow.swift; sourceTree = "<group>"; };
 		190EBCC529FF138000BA767D /* UserInterfaceSettingsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserInterfaceSettingsProvider.swift; sourceTree = "<group>"; };
@@ -3463,8 +3464,8 @@
 				38FEF413273B317A00574A46 /* HKUnit.swift in Sources */,
 				A33352ED40476125EBAC6EE0 /* CarbRatioEditorDataFlow.swift in Sources */,
 				17A9D0899046B45E87834820 /* CarbRatioEditorProvider.swift in Sources */,
-				118DF76B2C5ECBC60067FEB7 /* CancelOverrideIntent.swift in Sources */,
 				69B9A368029F7EB39F525422 /* CarbRatioEditorStateModel.swift in Sources */,
+				118DF76B2C5ECBC60067FEB7 /* CancelOverrideIntent.swift in Sources */,
 				38E44538274E411700EC9A94 /* Disk+[Data].swift in Sources */,
 				98641AF4F92123DA668AB931 /* CarbRatioEditorRootView.swift in Sources */,
 				BDF34F902C10CF8C00D51995 /* CoreDataStack.swift in Sources */,

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

@@ -131,6 +131,15 @@ final class BaseAPSManager: APSManager, Injectable {
     }
 
     private func subscribe() {
+        if settingsManager.settings.units == .mmolL {
+            let wasParsed = storage.parseOnFileSettingsToMgdL()
+            if wasParsed {
+                Task {
+                    try await makeProfiles()
+                }
+            }
+        }
+
         deviceDataManager.recommendsLoop
             .receive(on: processQueue)
             .sink { [weak self] in

+ 1 - 1
FreeAPS/Sources/APS/CGM/CGMType.swift

@@ -44,7 +44,7 @@ enum CGMType: String, JSON, CaseIterable, Identifiable {
     var externalLink: URL? {
         switch self {
         case .xdrip:
-            return URL(string: "https://github.com/JohanDegraeve/xdripswift")!
+            return URL(string: "https://xdrip4ios.readthedocs.io/")!
         default: return nil
         }
     }

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 1
FreeAPS/Sources/Localizations/Main/ar.lproj/Localizable.strings


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 1
FreeAPS/Sources/Localizations/Main/ca.lproj/Localizable.strings


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 1
FreeAPS/Sources/Localizations/Main/da.lproj/Localizable.strings


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 1
FreeAPS/Sources/Localizations/Main/de.lproj/Localizable.strings


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 1
FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 1
FreeAPS/Sources/Localizations/Main/es.lproj/Localizable.strings


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 1
FreeAPS/Sources/Localizations/Main/fi.lproj/Localizable.strings


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 1
FreeAPS/Sources/Localizations/Main/fr.lproj/Localizable.strings


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 1
FreeAPS/Sources/Localizations/Main/he.lproj/Localizable.strings


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 1
FreeAPS/Sources/Localizations/Main/hu.lproj/Localizable.strings


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 1
FreeAPS/Sources/Localizations/Main/it.lproj/Localizable.strings


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 1
FreeAPS/Sources/Localizations/Main/nb.lproj/Localizable.strings


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 1
FreeAPS/Sources/Localizations/Main/nl.lproj/Localizable.strings


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 1
FreeAPS/Sources/Localizations/Main/pl.lproj/Localizable.strings


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 1
FreeAPS/Sources/Localizations/Main/pt-BR.lproj/Localizable.strings


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 1
FreeAPS/Sources/Localizations/Main/pt-PT.lproj/Localizable.strings


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 1
FreeAPS/Sources/Localizations/Main/ru.lproj/Localizable.strings


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 1
FreeAPS/Sources/Localizations/Main/sk.lproj/Localizable.strings


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 1
FreeAPS/Sources/Localizations/Main/sv.lproj/Localizable.strings


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 1
FreeAPS/Sources/Localizations/Main/tr.lproj/Localizable.strings


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 1
FreeAPS/Sources/Localizations/Main/uk.lproj/Localizable.strings


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 1
FreeAPS/Sources/Localizations/Main/vi.lproj/Localizable.strings


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 1
FreeAPS/Sources/Localizations/Main/zh-Hans.lproj/Localizable.strings


+ 2 - 2
FreeAPS/Sources/Models/BGTargets.swift

@@ -1,8 +1,8 @@
 import Foundation
 
 struct BGTargets: JSON {
-    let units: GlucoseUnits
-    let userPreferredUnits: GlucoseUnits
+    var units: GlucoseUnits
+    var userPreferredUnits: GlucoseUnits
     var targets: [BGTargetEntry]
 }
 

+ 10 - 10
FreeAPS/Sources/Models/Determination.swift

@@ -2,10 +2,10 @@ import Foundation
 
 struct Determination: JSON, Equatable {
     let id: UUID?
-    let reason: String
+    var reason: String
     let units: Decimal?
     let insulinReq: Decimal?
-    let eventualBG: Int?
+    var eventualBG: Int?
     let sensitivityRatio: Decimal?
     let rate: Decimal?
     let duration: Decimal?
@@ -15,20 +15,20 @@ struct Determination: JSON, Equatable {
     var deliverAt: Date?
     let carbsReq: Decimal?
     let temp: TempType?
-    let bg: Decimal?
+    var bg: Decimal?
     let reservoir: Decimal?
-    let isf: Decimal?
+    var isf: Decimal?
     var timestamp: Date?
     let tdd: Decimal?
     let insulin: Insulin?
-    let current_target: Decimal?
+    var current_target: Decimal?
     let insulinForManualBolus: Decimal?
     let manualBolusErrorString: Decimal?
-    let minDelta: Decimal?
-    let expectedDelta: Decimal?
-    let minGuardBG: Decimal?
-    let minPredBG: Decimal?
-    let threshold: Decimal?
+    var minDelta: Decimal?
+    var expectedDelta: Decimal?
+    var minGuardBG: Decimal?
+    var minPredBG: Decimal?
+    var threshold: Decimal?
     let carbRatio: Decimal?
     let received: Bool?
 }

+ 3 - 3
FreeAPS/Sources/Models/InsulinSensitivities.swift

@@ -1,9 +1,9 @@
 import Foundation
 
 struct InsulinSensitivities: JSON {
-    let units: GlucoseUnits
-    let userPreferredUnits: GlucoseUnits
-    let sensitivities: [InsulinSensitivityEntry]
+    var units: GlucoseUnits
+    var userPreferredUnits: GlucoseUnits
+    var sensitivities: [InsulinSensitivityEntry]
 }
 
 extension InsulinSensitivities {

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 1
FreeAPS/Sources/Modules/GeneralSettings/View/UnitsLimitsSettingsRootView.swift


+ 5 - 5
FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift

@@ -582,7 +582,7 @@ extension MainChartView {
                 xEnd: .value("End", target.end),
                 y: .value("Value", targetLimited)
             )
-            .foregroundStyle(Color.purple.opacity(0.5)).lineStyle(.init(lineWidth: 8))
+            .foregroundStyle(Color.purple.opacity(0.75)).lineStyle(.init(lineWidth: 8))
         }
     }
 
@@ -596,9 +596,9 @@ extension MainChartView {
             RuleMark(
                 xStart: .value("Start", start, unit: .second),
                 xEnd: .value("End", end, unit: .second),
-                y: .value("Value", target)
+                y: .value("Value", units == .mgdL ? target : target.asMmolL)
             )
-            .foregroundStyle(Color.purple.opacity(0.6))
+            .foregroundStyle(Color.purple.opacity(0.4))
             .lineStyle(.init(lineWidth: 8))
 //            .annotation(position: .overlay, spacing: 0) {
 //                if let name = override.name {
@@ -616,9 +616,9 @@ extension MainChartView {
             RuleMark(
                 xStart: .value("Start", start, unit: .second),
                 xEnd: .value("End", end, unit: .second),
-                y: .value("Value", target)
+                y: .value("Value", units == .mgdL ? target : target.asMmolL)
             )
-            .foregroundStyle(Color.purple.opacity(0.4))
+            .foregroundStyle(Color.purple.opacity(0.25))
             .lineStyle(.init(lineWidth: 8))
 //            .annotation(position: .bottom, spacing: 0) {
 //                if let name = overrideRunStored.override?.name {

+ 1 - 8
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -103,13 +103,6 @@ extension Home {
             return dateFormatter
         }
 
-        private var spriteScene: SKScene {
-            let scene = SnowScene()
-            scene.scaleMode = .resizeFill
-            scene.backgroundColor = .clear
-            return scene
-        }
-
         private var color: LinearGradient {
             colorScheme == .dark ? LinearGradient(
                 gradient: Gradient(colors: [
@@ -823,7 +816,7 @@ extension Home {
                         List {
                             DefinitionRow(
                                 term: "Cone of Uncertainty",
-                                definition: "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.",
+                                definition: "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.\n\nTo modify the forecast display type, go to Trio Settings > Features > User Interface > Forecast Display Type.",
                                 color: Color.blue.opacity(0.5)
                             )
                         }

+ 7 - 1
FreeAPS/Sources/Modules/ISFEditor/ISFEditorProvider.swift

@@ -1,3 +1,5 @@
+import Foundation
+
 extension ISFEditor {
     final class Provider: BaseProvider, ISFEditorProvider {
         var profile: InsulinSensitivities {
@@ -12,7 +14,11 @@ extension ISFEditor {
             // migrate existing mmol/L Trio users from mmol/L settings to pure mg/dl settings
             if retrievedSensitivities.units == .mmolL || retrievedSensitivities.userPreferredUnits == .mmolL {
                 let convertedSensitivities = retrievedSensitivities.sensitivities.map { isf in
-                    InsulinSensitivityEntry(sensitivity: isf.sensitivity.asMgdL, offset: isf.offset, start: isf.start)
+                    InsulinSensitivityEntry(
+                        sensitivity: storage.parseSettingIfMmolL(value: isf.sensitivity),
+                        offset: isf.offset,
+                        start: isf.start
+                    )
                 }
                 retrievedSensitivities = InsulinSensitivities(
                     units: .mgdL,

+ 0 - 23
FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutConfigRootView.swift

@@ -160,29 +160,6 @@ extension NightscoutConfig {
                 if state.importStatus == .running {
                     CustomProgressView(text: "Importing Profile...")
                 }
-                //            .alert(isPresented: $importedHasRun) {
-                //                Alert(
-                //                    title: Text(
-                //                        (fetchedErrors.first?.error ?? "")
-                //                            .count < 4 ? "Settings imported" : "Import Error"
-                //                    ),
-                //                    message: Text(
-                //                        (fetchedErrors.first?.error ?? "").count < 4 ?
-                //                            NSLocalizedString(
-                //                                "\nNow please verify all of your new settings thoroughly: \n\n • DIA (Pump settings)\n • Basal Rates\n • Insulin Sensitivities\n • Carb Ratios\n • Target Glucose\n\n in Trio Settings -> Configuration.\n\nBad or invalid profile settings could have disastrous effects.",
-                //                                comment: "Imported Profiles Alert"
-                //                            ) :
-                //                            NSLocalizedString(
-                //                                fetchedErrors.first?.error ?? "",
-                //                                comment: "Import Error"
-                //                            )
-                //                    ),
-                //                    primaryButton: .destructive(
-                //                        Text("OK")
-                //                    ),
-                //                    secondaryButton: .cancel()
-                //                )
-                //            }
             }
             .fullScreenCover(isPresented: $state.isImportResultReviewPresented, content: {
                 NightscoutImportResultView(resolver: resolver, state: state)

+ 12 - 1
FreeAPS/Sources/Modules/OverrideConfig/View/OverrideRootView.swift

@@ -51,6 +51,17 @@ extension OverrideConfig {
             return formatter
         }
 
+        private var glucoseFormatter: NumberFormatter {
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            formatter.maximumFractionDigits = 0
+            if state.units == .mmolL {
+                formatter.maximumFractionDigits = 1
+            }
+            formatter.roundingMode = .halfUp
+            return formatter
+        }
+
         var body: some View {
             VStack {
                 Picker("Tab", selection: $state.selectedTab) {
@@ -271,7 +282,7 @@ extension OverrideConfig {
                     HStack {
                         Text("Target")
                         Spacer()
-                        TextFieldWithToolBar(text: $state.low, placeholder: "0", numberFormatter: formatter)
+                        TextFieldWithToolBar(text: $state.low, placeholder: "0", numberFormatter: glucoseFormatter)
                         Text(state.units.rawValue).foregroundColor(.secondary)
                     }
                     HStack {

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

@@ -221,6 +221,13 @@ extension Settings {
 //                        .navigationLink(to: .configEditor(file: OpenAPS.Settings.bgTargets), from: self)
 //                    Text("Sensitivities")
 //                        .navigationLink(to: .configEditor(file: OpenAPS.Settings.insulinSensitivities), from: self)
+//                    Text("Profile")
+//                        .navigationLink(to: .configEditor(file: OpenAPS.Settings.profile), from: self)
+//                    Text("Preferences")
+//                        .navigationLink(
+//                            to: .configEditor(file: OpenAPS.Settings.preferences),
+//                            from: self
+//                        )
 //                }.listRowBackground(Color.chart)
 
                 // TODO: remove this more or less entirely; add build-time flag to enable Middleware; add settings export feature

+ 8 - 1
FreeAPS/Sources/Modules/TargetsEditor/TargetsEditorProvider.swift

@@ -1,3 +1,5 @@
+import Foundation
+
 extension TargetsEditor {
     final class Provider: BaseProvider, TargetsEditorProvider {
         var profile: BGTargets {
@@ -8,7 +10,12 @@ extension TargetsEditor {
             // migrate existing mmol/L Trio users from mmol/L settings to pure mg/dl settings
             if retrievedTargets.units == .mmolL || retrievedTargets.userPreferredUnits == .mmolL {
                 let convertedTargets = retrievedTargets.targets.map { target in
-                    BGTargetEntry(low: target.low.asMgdL, high: target.high.asMgdL, start: target.start, offset: target.offset)
+                    BGTargetEntry(
+                        low: storage.parseSettingIfMmolL(value: target.low),
+                        high: storage.parseSettingIfMmolL(value: target.high),
+                        start: target.start,
+                        offset: target.offset
+                    )
                 }
                 retrievedTargets = BGTargets(units: .mgdL, userPreferredUnits: .mgdL, targets: convertedTargets)
                 saveProfile(retrievedTargets)

+ 109 - 2
FreeAPS/Sources/Services/Network/NightscoutManager.swift

@@ -83,6 +83,8 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     }
 
     private func subscribe() {
+        broadcaster.register(TempTargetsObserver.self, observer: self)
+
         _ = reachabilityManager.startListening(onQueue: processQueue) { status in
             debug(.nightscout, "Network status: \(status)")
         }
@@ -337,7 +339,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             return
         }
 
-        // Suggested/ Enacted
+        // Suggested / Enacted
         async let enactedDeterminationID = determinationStorage
             .fetchLastDeterminationObjectID(predicate: NSPredicate.enactedDeterminationsNotYetUploadedToNightscout)
         async let suggestedDeterminationID = determinationStorage
@@ -349,7 +351,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         async let fetchedIOBEntry = storage.retrieveAsync(OpenAPS.Monitor.iob, as: [IOBEntry].self)
         async let fetchedPumpStatus = storage.retrieveAsync(OpenAPS.Monitor.status, as: PumpStatus.self)
 
-        let (fetchedEnactedDetermination, fetchedSuggestedDetermination) = await (
+        var (fetchedEnactedDetermination, fetchedSuggestedDetermination) = await (
             determinationStorage.getOrefDeterminationNotYetUploadedToNightscout(enactedDeterminationID),
             determinationStorage.getOrefDeterminationNotYetUploadedToNightscout(suggestedDeterminationID)
         )
@@ -367,6 +369,10 @@ 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)
+            }
             // Check whether the last suggestion that was uploaded is the same that is fetched again when we are attempting to upload the enacted determination
             // Apparently we are too fast; so the flag update is not fast enough to have the predicate filter last suggestion out
             // If this check is truthy, set suggestion to nil so it's not uploaded again
@@ -377,6 +383,20 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             }
         }
 
+        if var fetchedEnacted = fetchedEnactedDetermination, settingsManager.settings.units == .mmolL {
+            var modifiedFetchedEnactedDetermination = fetchedEnactedDetermination
+            modifiedFetchedEnactedDetermination?
+                .reason = parseReasonGlucoseValuesToMmolL(fetchedEnacted.reason)
+
+            modifiedFetchedEnactedDetermination?.bg = fetchedEnacted.bg?.asMmolL
+            modifiedFetchedEnactedDetermination?.current_target = fetchedEnacted.current_target?.asMmolL
+            modifiedFetchedEnactedDetermination?.minGuardBG = fetchedEnacted.minGuardBG?.asMmolL
+            modifiedFetchedEnactedDetermination?.minPredBG = fetchedEnacted.minPredBG?.asMmolL
+            modifiedFetchedEnactedDetermination?.threshold = fetchedEnacted.threshold?.asMmolL
+
+            fetchedEnactedDetermination = modifiedFetchedEnactedDetermination
+        }
+
         // Gather all relevant data for OpenAPS Status
         let iob = await fetchedIOBEntry
         let openapsStatus = OpenAPSStatus(
@@ -912,3 +932,90 @@ extension Array {
         }
     }
 }
+
+extension BaseNightscoutManager: TempTargetsObserver {
+    func tempTargetsDidUpdate(_: [TempTarget]) {
+        Task.detached {
+            await self.uploadTempTargets()
+        }
+    }
+}
+
+extension BaseNightscoutManager {
+    /**
+     Converts glucose-related values in the given `reason` string to mmol/L, including ranges (e.g., `ISF: 54→54`), comparisons (e.g., `maxDelta 37 > 20% of BG 95`), and both positive and negative values (e.g., `Dev: -36`).
+
+     - Parameters:
+       - reason: The string containing glucose-related values to be converted.
+
+     - Returns:
+       A string with glucose values converted to mmol/L.
+
+     - Glucose tags handled: `ISF:`, `Target:`, `minPredBG`, `minGuardBG`, `IOBpredBG`, `COBpredBG`, `UAMpredBG`, `Dev:`, `maxDelta`, `BG`.
+     */
+    func parseReasonGlucoseValuesToMmolL(_ reason: String) -> String {
+        // Updated pattern to handle cases like minGuardBG 34, minGuardBG 34<70, and "maxDelta 37 > 20% of BG 95", and ensure "Target:" is handled correctly
+        let pattern =
+            "(ISF:\\s*-?\\d+→-?\\d+|Dev:\\s*-?\\d+|Target:\\s*-?\\d+|(?:minPredBG|minGuardBG|IOBpredBG|COBpredBG|UAMpredBG|maxDelta|BG)\\s*-?\\d+(?:<\\d+)?(?:>\\s*\\d+%\\s*of\\s*BG\\s*\\d+)?)"
+
+        let regex = try! NSRegularExpression(pattern: pattern)
+
+        func convertToMmolL(_ value: String) -> String {
+            if let glucoseValue = Double(value.replacingOccurrences(of: "[^\\d.-]", with: "", options: .regularExpression)) {
+                return glucoseValue.asMmolL.description
+            }
+            return value
+        }
+
+        let matches = regex.matches(in: reason, range: NSRange(reason.startIndex..., in: reason))
+        var updatedReason = reason
+
+        for match in matches.reversed() {
+            if let range = Range(match.range, in: reason) {
+                let glucoseValueString = String(reason[range])
+
+                if glucoseValueString.contains("→") {
+                    // Handle ISF case with an arrow (e.g., ISF: 54→54)
+                    let values = glucoseValueString.components(separatedBy: "→")
+                    let firstValue = convertToMmolL(values[0])
+                    let secondValue = convertToMmolL(values[1])
+                    let formattedGlucoseValueString = "\(values[0].components(separatedBy: ":")[0]): \(firstValue)→\(secondValue)"
+                    updatedReason.replaceSubrange(range, with: formattedGlucoseValueString)
+                } else if glucoseValueString.contains("<") {
+                    // Handle range case for minGuardBG like "minGuardBG 34<70"
+                    let values = glucoseValueString.components(separatedBy: "<")
+                    let firstValue = convertToMmolL(values[0])
+                    let secondValue = convertToMmolL(values[1])
+                    let formattedGlucoseValueString = "\(values[0].components(separatedBy: ":")[0]) \(firstValue)<\(secondValue)"
+                    updatedReason.replaceSubrange(range, with: formattedGlucoseValueString)
+                } else if glucoseValueString.contains(">"), glucoseValueString.contains("BG") {
+                    // Handle cases like "maxDelta 37 > 20% of BG 95"
+                    let pattern = "(\\d+) > \\d+% of BG (\\d+)"
+                    let matches = try! NSRegularExpression(pattern: pattern)
+                        .matches(in: glucoseValueString, range: NSRange(glucoseValueString.startIndex..., in: glucoseValueString))
+
+                    if let match = matches.first, match.numberOfRanges == 3 {
+                        let firstValueRange = Range(match.range(at: 1), in: glucoseValueString)!
+                        let secondValueRange = Range(match.range(at: 2), in: glucoseValueString)!
+
+                        let firstValue = convertToMmolL(String(glucoseValueString[firstValueRange]))
+                        let secondValue = convertToMmolL(String(glucoseValueString[secondValueRange]))
+
+                        let formattedGlucoseValueString = glucoseValueString.replacingOccurrences(
+                            of: "\(glucoseValueString[firstValueRange]) > 20% of BG \(glucoseValueString[secondValueRange])",
+                            with: "\(firstValue) > 20% of BG \(secondValue)"
+                        )
+                        updatedReason.replaceSubrange(range, with: formattedGlucoseValueString)
+                    }
+                } else {
+                    // General case for single glucose values like "Target: 100" or "minGuardBG 34"
+                    let parts = glucoseValueString.components(separatedBy: CharacterSet(charactersIn: ": "))
+                    let formattedValue = convertToMmolL(parts.last!.trimmingCharacters(in: .whitespaces))
+                    updatedReason.replaceSubrange(range, with: "\(parts[0]): \(formattedValue)")
+                }
+            }
+        }
+
+        return updatedReason
+    }
+}

+ 112 - 1
FreeAPS/Sources/Services/Storage/FileStorage.swift

@@ -14,8 +14,8 @@ protocol FileStorage {
     func remove(_ name: String)
     func rename(_ name: String, to newName: String)
     func transaction(_ exec: (FileStorage) -> Void)
-
     func urlFor(file: String) -> URL?
+    func parseOnFileSettingsToMgdL() -> Bool
 }
 
 final class BaseFileStorage: FileStorage {
@@ -150,3 +150,114 @@ final class BaseFileStorage: FileStorage {
         try? Disk.url(for: file, in: .documents)
     }
 }
+
+extension FileStorage {
+    private func correctUnitParsingOffsets(_ parsedValue: Decimal) -> Decimal {
+        Int(parsedValue) % 2 == 0 ? parsedValue : parsedValue + 1
+    }
+
+    func parseSettingIfMmolL(value: Decimal, threshold: Decimal = 39) -> Decimal {
+        value < threshold ? correctUnitParsingOffsets(value.asMgdL) : value
+    }
+
+    /// Parses mmol/L settings stored on file to mg/dL if necessary and updates the preferences, settings,  insulin sensitivities, and glucose targets.
+    /// - Returns: A boolean indicating whether any settings were parsed and updated.
+    func parseOnFileSettingsToMgdL() -> Bool {
+        debug(.businessLogic, "Check for mmol/L settings stored on file.")
+        var wasParsed = false
+
+        // Retrieve and parse preferences (Preferences struct)
+        if var preferences = retrieve(OpenAPS.Settings.preferences, as: Preferences.self) {
+            let initialThreshold = preferences.threshold_setting
+            let initialSMBTarget = preferences.enableSMB_high_bg_target
+            let initialExerciseTarget = preferences.halfBasalExerciseTarget
+
+            preferences.threshold_setting = parseSettingIfMmolL(value: preferences.threshold_setting)
+            preferences.enableSMB_high_bg_target = parseSettingIfMmolL(value: preferences.enableSMB_high_bg_target)
+            preferences.halfBasalExerciseTarget = parseSettingIfMmolL(value: preferences.halfBasalExerciseTarget)
+
+            if preferences.threshold_setting != initialThreshold ||
+                preferences.enableSMB_high_bg_target != initialSMBTarget ||
+                preferences.halfBasalExerciseTarget != initialExerciseTarget
+            {
+                debug(.businessLogic, "Preferences found in mmol/L. Parsing to mg/dL.")
+                save(preferences, as: OpenAPS.Settings.preferences)
+                wasParsed = true
+            } else {
+                debug(.businessLogic, "Preferences stored in mg/dL; no parsing required.")
+            }
+        }
+
+        // Retrieve and parse settings (FreeAPSSettings struct)
+        if var settings = retrieve(OpenAPS.Settings.settings, as: FreeAPSSettings.self) {
+            let initialHigh = settings.high
+            let initialLow = settings.low
+            let initialHighGlucose = settings.highGlucose
+            let initialLowGlucose = settings.lowGlucose
+
+            settings.high = parseSettingIfMmolL(value: settings.high)
+            settings.low = parseSettingIfMmolL(value: settings.low)
+            settings.highGlucose = parseSettingIfMmolL(value: settings.highGlucose)
+            settings.lowGlucose = parseSettingIfMmolL(value: settings.lowGlucose)
+
+            if settings.high != initialHigh ||
+                settings.low != initialLow ||
+                settings.highGlucose != initialHighGlucose ||
+                settings.lowGlucose != initialLowGlucose
+            {
+                debug(.businessLogic, "FreeAPSSettings found in mmol/L. Parsing to mg/dL.")
+                save(settings, as: OpenAPS.Settings.settings)
+                wasParsed = true
+            } else {
+                debug(.businessLogic, "FreeAPSSettings stored in mg/dL; no parsing required.")
+            }
+        }
+
+        // Retrieve and parse insulin sensitivities
+        if var sensitivities = retrieve(OpenAPS.Settings.insulinSensitivities, as: InsulinSensitivities.self),
+           sensitivities.units == .mmolL || sensitivities.userPreferredUnits == .mmolL
+        {
+            debug(.businessLogic, "Insulin sensitivities found in mmol/L. Parsing to mg/dL.")
+
+            sensitivities.sensitivities = sensitivities.sensitivities.map { isf in
+                InsulinSensitivityEntry(
+                    sensitivity: parseSettingIfMmolL(value: isf.sensitivity),
+                    offset: isf.offset,
+                    start: isf.start
+                )
+            }
+            sensitivities.units = .mgdL
+            sensitivities.userPreferredUnits = .mgdL
+
+            save(sensitivities, as: OpenAPS.Settings.insulinSensitivities)
+            wasParsed = true
+        } else {
+            debug(.businessLogic, "Insulin sensitivities stored in mg/dL; no parsing required.")
+        }
+
+        // Retrieve and parse glucose targets
+        if var glucoseTargets = retrieve(OpenAPS.Settings.bgTargets, as: BGTargets.self),
+           glucoseTargets.units == .mmolL || glucoseTargets.userPreferredUnits == .mmolL
+        {
+            debug(.businessLogic, "Glucose target profile found in mmol/L. Parsing to mg/dL.")
+
+            glucoseTargets.targets = glucoseTargets.targets.map { target in
+                BGTargetEntry(
+                    low: parseSettingIfMmolL(value: target.low),
+                    high: parseSettingIfMmolL(value: target.high),
+                    start: target.start,
+                    offset: target.offset
+                )
+            }
+            glucoseTargets.units = .mgdL
+            glucoseTargets.userPreferredUnits = .mgdL
+
+            save(glucoseTargets, as: OpenAPS.Settings.bgTargets)
+            wasParsed = true
+        } else {
+            debug(.businessLogic, "Glucose target profile stored in mg/dL; no parsing required.")
+        }
+
+        return wasParsed
+    }
+}

+ 82 - 95
FreeAPS/Sources/Views/TagCloudView.swift

@@ -7,16 +7,14 @@ struct TagCloudView: View {
     var tags: [String]
     var shouldParseToMmolL: Bool
 
-    @State private var totalHeight
-//          = CGFloat.zero       // << variant for ScrollView/List
-        = CGFloat.infinity // << variant for VStack
+    @State private var totalHeight = CGFloat.infinity // << variant for VStack
+
     var body: some View {
         VStack {
             GeometryReader { geometry in
                 self.generateContent(in: geometry)
             }
         }
-//        .frame(height: totalHeight)// << variant for ScrollView/List
         .frame(maxHeight: totalHeight) // << variant for VStack
     }
 
@@ -29,8 +27,7 @@ struct TagCloudView: View {
                 self.item(for: tag, isMmolL: shouldParseToMmolL)
                     .padding([.horizontal, .vertical], 2)
                     .alignmentGuide(.leading, computeValue: { d in
-                        if abs(width - d.width) > g.size.width
-                        {
+                        if abs(width - d.width) > g.size.width {
                             width = 0
                             height -= d.height
                         }
@@ -53,40 +50,6 @@ struct TagCloudView: View {
         }.background(viewHeightReader($totalHeight))
     }
 
-//    private func item(for textTag: String) -> some View {
-//        var colorOfTag: Color {
-//            switch textTag {
-//            case textTag where textTag.contains("SMB Delivery Ratio:"):
-//                return .uam
-//            case textTag where textTag.contains("Bolus"):
-//                return .green
-//            case textTag where textTag.contains("TDD:"),
-//                 textTag where textTag.contains("tdd_factor"),
-//                 textTag where textTag.contains("Sigmoid function"),
-//                 textTag where textTag.contains("Logarithmic formula"),
-//                 textTag where textTag.contains("AF:"),
-//                 textTag where textTag.contains("Autosens/Dynamic Limit:"),
-//                 textTag where textTag.contains("Dynamic ISF/CR"),
-//                 textTag where textTag.contains("Basal ratio"),
-//                 textTag where textTag.contains("SMB Ratio"):
-//                return .zt
-//            case textTag where textTag.contains("Middleware:"):
-//                return .red
-//            case textTag where textTag.contains("SMB Ratio"):
-//                return .orange
-//            default:
-//                return .insulin
-//            }
-//        }
-//
-//        return ZStack { Text(textTag)
-//            .padding(.vertical, 2)
-//            .padding(.horizontal, 4)
-//            .font(.subheadline)
-//            .background(colorOfTag.opacity(0.8))
-//            .foregroundColor(Color.white)
-//            .cornerRadius(2) }
-//    }
     private func item(for textTag: String, isMmolL: Bool) -> some View {
         var colorOfTag: Color {
             switch textTag {
@@ -113,61 +76,7 @@ struct TagCloudView: View {
             }
         }
 
-        func formattedTextTag(for tag: String) -> String {
-            // List of glucose-related tags
-            let glucoseTags = ["ISF:", "Target:", "minPredBG", "minGuardBG", "IOBpredBG", "COBpredBG", "UAMpredBG", "Dev:"]
-
-            var updatedTag = tag
-
-            // Apply conversion if necessary
-            for glucoseTag in glucoseTags {
-                if glucoseTag == "ISF:" {
-                    // Handle the special ISF case with the arrow
-                    if let range = updatedTag.range(of: "\(glucoseTag)\\s*\\d+→\\d+", options: .regularExpression) {
-                        let glucoseValueString = updatedTag[range]
-                        let values = glucoseValueString.components(separatedBy: "→")
-
-                        if let firstValue = Double(
-                            values[0]
-                                .components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
-                        ),
-                            let secondValue = Double(
-                                values[1]
-                                    .components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
-                            )
-                        {
-                            let formattedFirstValue = isMmolL ? Double(firstValue.asMmolL) : firstValue
-                            let formattedSecondValue = isMmolL ? Double(secondValue.asMmolL) : secondValue
-
-                            let formattedGlucoseValueString =
-                                "\(glucoseTag) \(formattedFirstValue)→\(formattedSecondValue)"
-                            updatedTag = updatedTag.replacingOccurrences(
-                                of: glucoseValueString,
-                                with: formattedGlucoseValueString
-                            )
-                        }
-                    }
-                } else {
-                    // General case for other glucose tags
-                    if let range = updatedTag.range(of: "\(glucoseTag)\\s*\\d+", options: .regularExpression) {
-                        let glucoseValueString = updatedTag[range]
-                        if let glucoseValue = Double(
-                            glucoseValueString
-                                .components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
-                        ) {
-                            let formattedValue = isMmolL ? Double(glucoseValue.asMmolL) : glucoseValue
-                            updatedTag = updatedTag.replacingOccurrences(
-                                of: glucoseValueString,
-                                with: "\(glucoseTag) \(formattedValue)"
-                            )
-                        }
-                    }
-                }
-            }
-            return updatedTag
-        }
-
-        let formattedTextTag = formattedTextTag(for: textTag)
+        let formattedTextTag = formatGlucoseTags(textTag, isMmolL: isMmolL)
 
         return ZStack {
             Text(formattedTextTag)
@@ -180,6 +89,84 @@ struct TagCloudView: View {
         }
     }
 
+    /**
+     Converts glucose-related values in the given `tag` string to mmol/L, including ranges (e.g., `ISF: 54→54`), comparisons (e.g., `maxDelta 37 > 20% of BG 95`), and both positive and negative values (e.g., `Dev: -36`).
+
+     - Parameters:
+       - tag: The string containing glucose-related values to be converted.
+       - isMmolL: A Boolean flag indicating whether to convert values to mmol/L.
+
+     - Returns:
+       A string with glucose values converted to mmol/L.
+
+     - Glucose tags handled: `ISF:`, `Target:`, `minPredBG`, `minGuardBG`, `IOBpredBG`, `COBpredBG`, `UAMpredBG`, `Dev:`, `maxDelta`, `BG`.
+     */
+    private func formatGlucoseTags(_ tag: String, isMmolL: Bool) -> String {
+        // Updated pattern to handle cases like minGuardBG 34, minGuardBG 34<70, "maxDelta 37 > 20% of BG 95", and ensure "Target:" is handled correctly
+        let pattern =
+            "(ISF:\\s*-?\\d+→-?\\d+|Dev:\\s*-?\\d+|Target:\\s*-?\\d+|(?:minPredBG|minGuardBG|IOBpredBG|COBpredBG|UAMpredBG|maxDelta|BG)\\s*-?\\d+(?:<\\d+)?(?:>\\s*\\d+%\\s*of\\s*BG\\s*\\d+)?)"
+
+        let regex = try! NSRegularExpression(pattern: pattern)
+
+        func convertToMmolL(_ value: String) -> String {
+            if let glucoseValue = Double(value.replacingOccurrences(of: "[^\\d.-]", with: "", options: .regularExpression)) {
+                return isMmolL ? glucoseValue.asMmolL.description : value
+            }
+            return value
+        }
+
+        let matches = regex.matches(in: tag, range: NSRange(tag.startIndex..., in: tag))
+        var updatedTag = tag
+
+        for match in matches.reversed() {
+            if let range = Range(match.range, in: tag) {
+                let glucoseValueString = String(tag[range])
+
+                if glucoseValueString.contains("→") {
+                    // Handle ISF case with an arrow (e.g., ISF: 54→54)
+                    let values = glucoseValueString.components(separatedBy: "→")
+                    let firstValue = convertToMmolL(values[0])
+                    let secondValue = convertToMmolL(values[1])
+                    let formattedGlucoseValueString = "\(values[0].components(separatedBy: ":")[0]): \(firstValue)→\(secondValue)"
+                    updatedTag.replaceSubrange(range, with: formattedGlucoseValueString)
+                } else if glucoseValueString.contains("<") {
+                    // Handle range case for minGuardBG like "minGuardBG 34<70"
+                    let values = glucoseValueString.components(separatedBy: "<")
+                    let firstValue = convertToMmolL(values[0])
+                    let secondValue = convertToMmolL(values[1])
+                    let formattedGlucoseValueString = "\(values[0].components(separatedBy: ":")[0]) \(firstValue)<\(secondValue)"
+                    updatedTag.replaceSubrange(range, with: formattedGlucoseValueString)
+                } else if glucoseValueString.contains(">"), glucoseValueString.contains("BG") {
+                    // Handle cases like "maxDelta 37 > 20% of BG 95"
+                    let pattern = "(\\d+) > \\d+% of BG (\\d+)"
+                    let matches = try! NSRegularExpression(pattern: pattern)
+                        .matches(in: glucoseValueString, range: NSRange(glucoseValueString.startIndex..., in: glucoseValueString))
+
+                    if let match = matches.first, match.numberOfRanges == 3 {
+                        let firstValueRange = Range(match.range(at: 1), in: glucoseValueString)!
+                        let secondValueRange = Range(match.range(at: 2), in: glucoseValueString)!
+
+                        let firstValue = convertToMmolL(String(glucoseValueString[firstValueRange]))
+                        let secondValue = convertToMmolL(String(glucoseValueString[secondValueRange]))
+
+                        let formattedGlucoseValueString = glucoseValueString.replacingOccurrences(
+                            of: "\(glucoseValueString[firstValueRange]) > 20% of BG \(glucoseValueString[secondValueRange])",
+                            with: "\(firstValue) > 20% of BG \(secondValue)"
+                        )
+                        updatedTag.replaceSubrange(range, with: formattedGlucoseValueString)
+                    }
+                } else {
+                    // General case for single glucose values like "Target: 100" or "minGuardBG 34"
+                    let parts = glucoseValueString.components(separatedBy: CharacterSet(charactersIn: ": "))
+                    let formattedValue = convertToMmolL(parts.last!.trimmingCharacters(in: .whitespaces))
+                    updatedTag.replaceSubrange(range, with: "\(parts[0]): \(formattedValue)")
+                }
+            }
+        }
+
+        return updatedTag
+    }
+
     private func viewHeightReader(_ binding: Binding<CGFloat>) -> some View {
         GeometryReader { geometry -> Color in
             let rect = geometry.frame(in: .local)