ソースを参照

Merge pull request #878 from mountrcg/easyTTfix

Fix Add & Edit TT to have target value change HBT instead of sensitivity
Deniz Cengiz 5 ヶ月 前
コミット
f69e7bbefb

+ 20 - 16
Trio.xcodeproj/project.pbxproj

@@ -266,6 +266,7 @@
 		491D6FBE2D56741C00C49F67 /* TempTargetRunStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FB92D56741C00C49F67 /* TempTargetRunStored+CoreDataClass.swift */; };
 		491D6FBF2D56741C00C49F67 /* TempTargetRunStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FBA2D56741C00C49F67 /* TempTargetRunStored+CoreDataProperties.swift */; };
 		491D6FC02D56741C00C49F67 /* TempTargetStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FBB2D56741C00C49F67 /* TempTargetStored+CoreDataClass.swift */; };
+		49239B432EEA27AD00469145 /* TempTargetCalculations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49239B422EEA27AD00469145 /* TempTargetCalculations.swift */; };
 		49B9B57F2D5768D2009C6B59 /* AdjustmentStored+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49B9B57E2D5768D2009C6B59 /* AdjustmentStored+Helper.swift */; };
 		5075C1608E6249A51495C422 /* TargetsEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BDEA2DC60EDE0A3CA54DC73 /* TargetsEditorProvider.swift */; };
 		53F2382465BF74DB1A967C8B /* PumpConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8630D58BDAD6D9C650B9B39 /* PumpConfigProvider.swift */; };
@@ -1092,6 +1093,7 @@
 		491D6FBA2D56741C00C49F67 /* TempTargetRunStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetRunStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		491D6FBB2D56741C00C49F67 /* TempTargetStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		491D6FBC2D56741C00C49F67 /* TempTargetStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
+		49239B422EEA27AD00469145 /* TempTargetCalculations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetCalculations.swift; sourceTree = "<group>"; };
 		49B9B57E2D5768D2009C6B59 /* AdjustmentStored+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentStored+Helper.swift"; sourceTree = "<group>"; };
 		4DD795BA46B193644D48138C /* TargetsEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorRootView.swift; sourceTree = "<group>"; };
 		505E09DC17A0C3D0AF4B66FE /* ISFEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ISFEditorStateModel.swift; sourceTree = "<group>"; };
@@ -2418,44 +2420,45 @@
 		388E5A5A25B6F05F0019842D /* Helpers */ = {
 			isa = PBXGroup;
 			children = (
-				DDD5889C2DDDC9A900C8848D /* TimeAgoFormatter.swift */,
-				DD82D4B72DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift */,
-				DDCAE8322D78D49C00B1BB51 /* TherapySettingsUtil.swift */,
-				BD249DA62D42FE3800412DEB /* Calendar+GlucoseStatsChart.swift */,
+				E06B9119275B5EEA003C04B6 /* Array+Extension.swift */,
 				DD73FA0E2D74F57300D19D1E /* BackgroundTask+Helper.swift */,
+				DD1DB7CB2BECCA1F0048B367 /* BuildDetails.swift */,
+				FE66D16A291F74F8005D6F77 /* Bundle+Extensions.swift */,
+				BD249DA62D42FE3800412DEB /* Calendar+GlucoseStatsChart.swift */,
 				CEF1ED6A2D58FB4600FAF41E /* CGMOptions.swift */,
-				C2A0A42E2CE0312C003B98E8 /* ConstantValues.swift */,
-				DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */,
 				38F37827261260DC009DB701 /* Color+Extensions.swift */,
 				389ECE042601144100D86C4F /* ConcurrentMap.swift */,
+				C2A0A42E2CE0312C003B98E8 /* ConstantValues.swift */,
 				38192E0C261BAF980094D973 /* ConvenienceExtensions.swift */,
+				BD1661302B82ADAB00256551 /* CustomProgressView.swift */,
+				581516A32BCED84A00BF67D7 /* DebuggingIdentifiers.swift */,
 				3871F39E25ED895A0013ECB5 /* Decimal+Extensions.swift */,
 				38C4D33625E9A1A200D30B77 /* DispatchQueue+Extensions.swift */,
 				389487392614928B004DF424 /* DispatchTimer.swift */,
+				DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */,
 				3811DE5425C9D4D500A708ED /* Formatters.swift */,
 				38FEF412273B317A00574A46 /* HKUnit.swift */,
 				38B4F3AE25E2979F00E76A18 /* IndexedCollection.swift */,
 				389A571F26079BAA00BC102F /* Interpolation.swift */,
 				388E5A5B25B6F0770019842D /* JSON.swift */,
 				38A00B2225FC2B55006BC0B0 /* LRUCache.swift */,
+				582DF9782C8CE1E5001F516D /* MainChartHelper.swift */,
 				38FCF3D525E8FDF40078B0D1 /* MD5.swift */,
 				38E98A2C25F52DC400C0CED0 /* NSLocking+Extensions.swift */,
 				38C4D33925E9A1ED00D30B77 /* NSObject+AssociatedValues.swift */,
 				3811DE5725C9D4D500A708ED /* ProgressBar.swift */,
+				DD82D4B72DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift */,
+				3811DEE325CA063400A708ED /* PropertyWrappers */,
 				3811DE5525C9D4D500A708ED /* Publisher.swift */,
+				DD6B7CB12C7B6F0800B75029 /* Rounding.swift */,
+				CEA4F62229BE10F70011ADF7 /* SavitzkyGolayFilter.swift */,
 				38E98A3625F5509500C0CED0 /* String+Extensions.swift */,
-				3811DEE325CA063400A708ED /* PropertyWrappers */,
-				E06B9119275B5EEA003C04B6 /* Array+Extension.swift */,
+				49239B422EEA27AD00469145 /* TempTargetCalculations.swift */,
+				DDCAE8322D78D49C00B1BB51 /* TherapySettingsUtil.swift */,
+				DDD5889C2DDDC9A900C8848D /* TimeAgoFormatter.swift */,
+				BD2FF19F2AE29D43005D1C5D /* ToggleStyles.swift */,
 				CEB434E428B8FF5D00B70274 /* UIColor.swift */,
-				FE66D16A291F74F8005D6F77 /* Bundle+Extensions.swift */,
 				FEFFA7A12929FE49007B8193 /* UIDevice+Extensions.swift */,
-				CEA4F62229BE10F70011ADF7 /* SavitzkyGolayFilter.swift */,
-				BD2FF19F2AE29D43005D1C5D /* ToggleStyles.swift */,
-				BD1661302B82ADAB00256551 /* CustomProgressView.swift */,
-				581516A32BCED84A00BF67D7 /* DebuggingIdentifiers.swift */,
-				DD1DB7CB2BECCA1F0048B367 /* BuildDetails.swift */,
-				DD6B7CB12C7B6F0800B75029 /* Rounding.swift */,
-				582DF9782C8CE1E5001F516D /* MainChartHelper.swift */,
 			);
 			path = Helpers;
 			sourceTree = "<group>";
@@ -4270,6 +4273,7 @@
 				DDF691012DA2CA11008BF16C /* AppDiagnosticsDataFlow.swift in Sources */,
 				DDA6E3202D258E0500C2988C /* OverrideHelpView.swift in Sources */,
 				DDA6E2502D22187500C2988C /* ChartLegendView.swift in Sources */,
+				49239B432EEA27AD00469145 /* TempTargetCalculations.swift in Sources */,
 				3811DEAF25C9D88300A708ED /* KeyValueStorage.swift in Sources */,
 				DDD6D4D32CDE90720029439A /* EstimatedA1cDisplayUnit.swift in Sources */,
 				DDA6E3572D25988500C2988C /* ContactImageHelpView.swift in Sources */,

+ 22 - 6
Trio/Sources/APS/OpenAPS/OpenAPS.swift

@@ -534,15 +534,31 @@ final class OpenAPS {
 
         // Check for active Temp Targets and adjust HBT if necessary
         try await context.perform {
-            // Check if a Temp Target is active and if its HBT differs from user preferences
+            // Check if a Temp Target is active and check HBT differs from setting and adjust
             if let activeTempTarget = try self.fetchActiveTempTargets().first,
                activeTempTarget.enabled,
-               let activeHBT = activeTempTarget.halfBasalTarget?.decimalValue,
-               activeHBT != defaultHalfBasalTarget
+               let targetValue = activeTempTarget.target?.decimalValue
             {
-                // Overwrite the HBT in preferences
-                adjustedPreferences.halfBasalExerciseTarget = activeHBT
-                debug(.openAPS, "Updated halfBasalExerciseTarget to active Temp Target value: \(activeHBT)")
+                // Compute effective HBT - handles both custom HBT and standard TT (where HBT might need adjustment)
+                let effectiveHBT = TempTargetCalculations.computeEffectiveHBT(
+                    tempTargetHalfBasalTarget: activeTempTarget.halfBasalTarget?.decimalValue,
+                    settingHalfBasalTarget: defaultHalfBasalTarget,
+                    target: targetValue,
+                    autosensMax: preferences.autosensMax
+                )
+
+                if let effectiveHBT, effectiveHBT != defaultHalfBasalTarget {
+                    adjustedPreferences.halfBasalExerciseTarget = effectiveHBT
+                    let percentage = Int(TempTargetCalculations.computeAdjustedPercentage(
+                        halfBasalTarget: effectiveHBT,
+                        target: targetValue,
+                        autosensMax: preferences.autosensMax
+                    ))
+                    debug(
+                        .openAPS,
+                        "TempTarget: target=\(targetValue), HBT=\(defaultHalfBasalTarget), effectiveHBT=\(effectiveHBT), percentage=\(percentage)%, adjustmentType=Custom"
+                    )
+                }
             }
             // Overwrite the lowTTlowersSens if autosensMax does not support it
             if preferences.lowTemptargetLowersSensitivity, preferences.autosensMax <= 1 {

+ 104 - 0
Trio/Sources/Helpers/TempTargetCalculations.swift

@@ -0,0 +1,104 @@
+import Foundation
+
+/// Helper functions for TempTarget sensitivity calculations.
+/// These are used across the app (UI, OpenAPS) to ensure consistent behavior.
+enum TempTargetCalculations {
+    /// The minimum allowed sensitivity ratio for TempTargets (15%)
+    static let minSensitivityRatioTT: Double = 15
+
+    /// The normal target glucose value used as reference (100 mg/dL)
+    static let normalTarget: Decimal = 100
+
+    /// Computes the adjusted percentage (clamped to minSensitivityRatioTT).
+    /// - Parameters:
+    ///   - halfBasalTarget: The half basal target value
+    ///   - target: The target glucose value
+    ///   - autosensMax: The maximum autosens multiplier from settings
+    /// - Returns: The clamped percentage (minimum is minSensitivityRatioTT)
+    static func computeAdjustedPercentage(
+        halfBasalTarget: Decimal,
+        target: Decimal,
+        autosensMax: Decimal
+    ) -> Double {
+        let rawPercentage = computeRawPercentage(
+            halfBasalTarget: halfBasalTarget,
+            target: target,
+            autosensMax: autosensMax
+        )
+        return max(rawPercentage, minSensitivityRatioTT)
+    }
+
+    /// Computes the raw (unclamped) percentage - private helper.
+    private static func computeRawPercentage(
+        halfBasalTarget: Decimal,
+        target: Decimal,
+        autosensMax: Decimal
+    ) -> Double {
+        let deviationFromNormal = halfBasalTarget - normalTarget
+        let adjustmentFactor = deviationFromNormal + (target - normalTarget)
+        let adjustmentRatio: Decimal = (deviationFromNormal * adjustmentFactor <= 0)
+            ? autosensMax
+            : deviationFromNormal / adjustmentFactor
+        return Double(min(adjustmentRatio, autosensMax) * 100)
+    }
+
+    /// Computes the half-basal target needed to achieve a given percentage.
+    /// - Parameters:
+    ///   - target: The target glucose value
+    ///   - percentage: The desired sensitivity percentage
+    /// - Returns: The half basal target value that yields the given percentage
+    static func computeHalfBasalTarget(
+        target: Decimal,
+        percentage: Double
+    ) -> Double {
+        var adjustmentPercentage = percentage
+        if adjustmentPercentage < minSensitivityRatioTT {
+            adjustmentPercentage = minSensitivityRatioTT
+        }
+        let adjustmentRatio = Decimal(adjustmentPercentage / 100)
+        var halfBasalTargetValue: Decimal = 160 // default
+        if adjustmentRatio != 1 {
+            halfBasalTargetValue = ((2 * adjustmentRatio * normalTarget) - normalTarget - (adjustmentRatio * target)) /
+                (adjustmentRatio - 1)
+        }
+        return round(Double(halfBasalTargetValue))
+    }
+
+    /// Determines the effective HBT to use for a TempTarget.
+    /// If the stored HBT is nil (standard TT) and using settings HBT would result in <= 15%,
+    /// calculates an adjusted HBT. Otherwise returns the stored HBT or nil.
+    /// - Parameters:
+    ///   - tempTargetHalfBasalTarget: The HBT stored with the TempTarget (nil for standard TT)
+    ///   - settingHalfBasalTarget: The HBT from user settings
+    ///   - target: The target glucose value
+    ///   - autosensMax: The maximum autosens multiplier from settings
+    /// - Returns: The effective HBT to use, or nil if settings HBT should be used as-is
+    static func computeEffectiveHBT(
+        tempTargetHalfBasalTarget: Decimal?,
+        settingHalfBasalTarget: Decimal,
+        target: Decimal,
+        autosensMax: Decimal
+    ) -> Decimal? {
+        // If TempTarget has a stored HBT, use it directly
+        if let tempTargetHalfBasalTarget {
+            return tempTargetHalfBasalTarget
+        }
+
+        // For standard TT (no stored HBT), check if settings HBT would result in <= minimum
+        let rawPercentage = computeRawPercentage(
+            halfBasalTarget: settingHalfBasalTarget,
+            target: target,
+            autosensMax: autosensMax
+        )
+
+        // If raw percentage is at or below minimum, calculate an adjusted HBT
+        if rawPercentage <= minSensitivityRatioTT {
+            return Decimal(computeHalfBasalTarget(
+                target: target,
+                percentage: minSensitivityRatioTT
+            ))
+        }
+
+        return nil
+    }
+}

+ 26 - 2
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -10037,7 +10037,6 @@
       }
     },
     "%lld h" : {
-      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -115640,7 +115639,12 @@
         }
       }
     },
+    "Furthermore, you could adjust that sensitivity change independently from the Half Basal Exercise Target specified in Algorithm > Target Behavior settings by deliberately setting a customized Insulin Percentage for a Temp Target." : {
+      "comment" : "A paragraph explaining how to set a custom insulin percentage for a temporary target.",
+      "isCommentAutoGenerated" : true
+    },
     "Furthermore, you could adjust that sensitivity change independently from the Half Basal Exercise Target specified in Algorithm > Target Behavior settings by deliberatly setting a customized Insulin Percentage for a Temp Target." : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -169875,6 +169879,10 @@
         }
       }
     },
+    "Note: Autosens Min and Autosens Max settings do not apply symmetrically to Temp Target sensitivity adjustments. Autosens Max limits how much sensitivity can be decreased (more insulin), but Autosens Min does not override the 15% floor for increased sensitivity (less insulin)." : {
+      "comment" : "A note explaining the asymmetry in how Autosens Min and Max affect Temp Target sensitivity adjustments.",
+      "isCommentAutoGenerated" : true
+    },
     "Note: Basal may be resumed if there is negative IOB and glucose is rising faster than the forecast." : {
       "localizations" : {
         "bg" : {
@@ -202127,6 +202135,14 @@
         }
       }
     },
+    "Sensitivity adjustments from Temp Targets have a hard-coded minimum of 15%. This means even very high Temp Targets cannot reduce insulin delivery below 15% of normal." : {
+      "comment" : "A description of the 15% floor for Temp Target sensitivity adjustments.",
+      "isCommentAutoGenerated" : true
+    },
+    "Sensitivity Limits" : {
+      "comment" : "A label displayed above a section explaining the sensitivity limits of temp targets.",
+      "isCommentAutoGenerated" : true
+    },
     "Sensitivity Raises Target" : {
       "comment" : "Sensitivity Raises Target",
       "localizations" : {
@@ -232301,6 +232317,10 @@
         }
       }
     },
+    "This 15% floor is a safety limit inherited from oref (OpenAPS reference design) and AndroidAPS. It prevents Temp Targets from reducing insulin to dangerously low levels." : {
+      "comment" : "A text describing the 15% floor that prevents Temp Targets from reducing insulin to dangerously low levels.",
+      "isCommentAutoGenerated" : true
+    },
     "This adjusted ISF is temporary, will change with the next loop cycle, and should not be directly used as your profile ISF value." : {
       "localizations" : {
         "bg" : {
@@ -232662,6 +232682,10 @@
         }
       }
     },
+    "This asymmetry exists because reducing insulin delivery during exercise, normally realized by using high Temp Targets, typically requires a higher insulin reduction than what autosens would identify in a regular dayly routine." : {
+      "comment" : "A note explaining the asymmetry in the Temp Target sensitivity adjustment rules.",
+      "isCommentAutoGenerated" : true
+    },
     "This cap prevents the system from overestimating how much insulin is needed when carb absorption isn't visible, offering a safeguard for accurate dosing." : {
       "localizations" : {
         "bg" : {
@@ -270545,5 +270569,5 @@
       }
     }
   },
-  "version" : "1.0"
+  "version" : "1.1"
 }

+ 1 - 1
Trio/Sources/Models/DecimalPickerSettings.swift

@@ -72,7 +72,7 @@ struct DecimalPickerSettings {
     var halfBasalExerciseTarget = PickerSetting(
         value: 160,
         step: 5,
-        min: 100,
+        min: 105, // must be >100 to work mathematically, 5mg/dl steps are fine enough as this is just a calculation helper value
         max: 300,
         type: PickerSetting.PickerSettingType.glucose
     )

+ 20 - 37
Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+TempTargets.swift

@@ -138,6 +138,11 @@ extension Adjustments.StateModel {
         let date = self.date
         guard date > Date() else { return }
 
+        let adjustmentType = halfBasalTarget == settingHalfBasalTarget ? "Standard" : "Custom"
+        debug(
+            .default,
+            "TempTarget: target=\(tempTargetTarget), HBT=\(settingHalfBasalTarget), effectiveHBT=\(halfBasalTarget), percentage=\(Int(percentage))%, adjustmentType=\(adjustmentType)"
+        )
         let tempTarget = TempTarget(
             name: tempTargetName,
             createdAt: date,
@@ -203,6 +208,11 @@ extension Adjustments.StateModel {
     /// Saves a custom Temp Target and disables existing ones.
     func saveCustomTempTarget() async throws {
         await disableAllActiveTempTargets(createTempTargetRunEntry: true)
+        let adjustmentType = halfBasalTarget == settingHalfBasalTarget ? "Standard" : "Custom"
+        debug(
+            .default,
+            "TempTarget: target=\(tempTargetTarget), HBT=\(settingHalfBasalTarget), effectiveHBT=\(halfBasalTarget), percentage=\(Int(percentage))%, adjustmentType=\(adjustmentType)"
+        )
         let tempTarget = TempTarget(
             name: tempTargetName,
             /// We don't need to use the state var date here as we are using a different function for scheduled Temp Targets 'saveScheduledTempTarget()'
@@ -225,6 +235,11 @@ extension Adjustments.StateModel {
 
     /// Creates a new Temp Target preset.
     func saveTempTargetPreset() async throws {
+        let adjustmentType = halfBasalTarget == settingHalfBasalTarget ? "Standard" : "Custom"
+        debug(
+            .default,
+            "TempTarget: target=\(tempTargetTarget), HBT=\(settingHalfBasalTarget), effectiveHBT=\(halfBasalTarget), percentage=\(Int(percentage))%, adjustmentType=\(adjustmentType)"
+        )
         let tempTarget = TempTarget(
             name: tempTargetName,
             createdAt: Date(),
@@ -384,35 +399,19 @@ extension Adjustments.StateModel {
 
     // MARK: - Calculations
 
-    /// Computes the half-basal target based on the current settings.
-    func computeHalfBasalTarget(
-        usingTarget initialTarget: Decimal? = nil,
-        usingPercentage initialPercentage: Double? = nil
-    ) -> Double {
-        let adjustmentPercentage = initialPercentage ?? percentage
-        let adjustmentRatio = Decimal(adjustmentPercentage / 100)
-        let tempTargetValue: Decimal = initialTarget ?? tempTargetTarget
-        var halfBasalTargetValue = halfBasalTarget
-        if adjustmentRatio != 1 {
-            halfBasalTargetValue = ((2 * adjustmentRatio * normalTarget) - normalTarget - (adjustmentRatio * tempTargetValue)) /
-                (adjustmentRatio - 1)
-        }
-        return round(Double(halfBasalTargetValue))
-    }
-
     /// Determines if sensitivity adjustment is enabled based on target.
     func isAdjustSensEnabled(usingTarget initialTarget: Decimal? = nil) -> Bool {
         let target = initialTarget ?? tempTargetTarget
-        if target < normalTarget, lowTTlowersSens && autosensMax > 1 { return true }
-        if target > normalTarget, highTTraisesSens || isExerciseModeActive { return true }
+        if target < TempTargetCalculations.normalTarget, lowTTlowersSens && autosensMax > 1 { return true }
+        if target > TempTargetCalculations.normalTarget, highTTraisesSens || isExerciseModeActive { return true }
         return false
     }
 
     /// Computes the low value for the slider based on the target.
     func computeSliderLow(usingTarget initialTarget: Decimal? = nil) -> Double {
         let calcTarget = initialTarget ?? tempTargetTarget
-        guard calcTarget != 0 else { return 15 } // oref defined maximum sensitivity
-        let minSens = calcTarget < normalTarget ? 105 : 15
+        guard calcTarget != 0 else { return TempTargetCalculations.minSensitivityRatioTT } // oref defined maximum sensitivity
+        let minSens = calcTarget < TempTargetCalculations.normalTarget ? 105 : TempTargetCalculations.minSensitivityRatioTT
         return Double(max(0, minSens))
     }
 
@@ -421,25 +420,9 @@ extension Adjustments.StateModel {
         let calcTarget = initialTarget ?? tempTargetTarget
         guard calcTarget != 0
         else { return Double(autosensMax * 100) } // oref defined limit for increased insulin delivery
-        let maxSens = calcTarget > normalTarget ? 95 : Double(autosensMax * 100)
+        let maxSens = calcTarget > TempTargetCalculations.normalTarget ? 95 : Double(autosensMax * 100)
         return maxSens
     }
-
-    /// Computes the adjusted percentage for the slider.
-    func computeAdjustedPercentage(
-        usingHBT initialHalfBasalTarget: Decimal? = nil,
-        usingTarget initialTarget: Decimal? = nil
-    ) -> Double {
-        let halfBasalTargetValue = initialHalfBasalTarget ?? halfBasalTarget
-        let calcTarget = initialTarget ?? tempTargetTarget
-        let deviationFromNormal = halfBasalTargetValue - normalTarget
-
-        let adjustmentFactor = deviationFromNormal + (calcTarget - normalTarget)
-        let adjustmentRatio: Decimal = (deviationFromNormal * adjustmentFactor <= 0) ? autosensMax : deviationFromNormal /
-            adjustmentFactor
-
-        return Double(min(adjustmentRatio, autosensMax) * 100).rounded()
-    }
 }
 
 enum TempTargetSensitivityAdjustmentType: String, CaseIterable {

+ 10 - 3
Trio/Sources/Modules/Adjustments/AdjustmentsStateModel.swift

@@ -49,7 +49,6 @@ extension Adjustments {
         var units: GlucoseUnits = .mgdL
 
         // Temp Target Properties
-        let normalTarget: Decimal = 100
         var tempTargetDuration: Decimal = 0
         var tempTargetName: String = ""
         var tempTargetTarget: Decimal = 100
@@ -158,7 +157,11 @@ extension Adjustments {
             highTTraisesSens = settingsManager.preferences.highTemptargetRaisesSensitivity
             isExerciseModeActive = settingsManager.preferences.exerciseMode
             lowTTlowersSens = settingsManager.preferences.lowTemptargetLowersSensitivity
-            percentage = computeAdjustedPercentage()
+            percentage = TempTargetCalculations.computeAdjustedPercentage(
+                halfBasalTarget: halfBasalTarget,
+                target: tempTargetTarget,
+                autosensMax: autosensMax
+            )
             Task {
                 await getCurrentGlucoseTarget()
             }
@@ -268,7 +271,11 @@ extension Adjustments.StateModel: SettingsObserver, PreferencesObserver {
         highTTraisesSens = settingsManager.preferences.highTemptargetRaisesSensitivity
         isExerciseModeActive = settingsManager.preferences.exerciseMode
         lowTTlowersSens = settingsManager.preferences.lowTemptargetLowersSensitivity
-        percentage = computeAdjustedPercentage()
+        percentage = TempTargetCalculations.computeAdjustedPercentage(
+            halfBasalTarget: halfBasalTarget,
+            target: tempTargetTarget,
+            autosensMax: autosensMax
+        )
         Task {
             await getCurrentGlucoseTarget()
         }

+ 25 - 5
Trio/Sources/Modules/Adjustments/View/TempTargets/AddTempTargetForm.swift

@@ -61,7 +61,7 @@ struct AddTempTargetForm: View {
             }
             .onAppear {
                 targetStep = state.units == .mgdL ? 5 : 9
-                state.tempTargetTarget = state.normalTarget
+                state.tempTargetTarget = TempTargetCalculations.normalTarget
             }
             .sheet(isPresented: $state.isHelpSheetPresented) {
                 TempTargetHelpView(state: state, helpSheetDetent: $state.helpSheetDetent)
@@ -101,12 +101,25 @@ struct AddTempTargetForm: View {
                     toggleScrollWheel: toggleScrollWheel
                 )
                 .onChange(of: state.tempTargetTarget) {
-                    state.percentage = state.computeAdjustedPercentage()
+                    // when first setting a custom sensitivity the settings HBT is used and therefore we calculate the sensitivity
+                    if state.halfBasalTarget == state.settingHalfBasalTarget {
+                        state.percentage = TempTargetCalculations.computeAdjustedPercentage(
+                            halfBasalTarget: state.halfBasalTarget,
+                            target: state.tempTargetTarget,
+                            autosensMax: state.autosensMax
+                        )
+                    } else {
+                        // else when changing target value and the already adjusted HBT is used, keep the sensitivity and adjust the HBT instead
+                        state.halfBasalTarget = Decimal(TempTargetCalculations.computeHalfBasalTarget(
+                            target: state.tempTargetTarget,
+                            percentage: state.percentage
+                        ))
+                    }
                 }
             }
             .listRowBackground(Color.chart)
 
-            if state.tempTargetTarget != state.normalTarget {
+            if state.tempTargetTarget != TempTargetCalculations.normalTarget {
                 if state.isAdjustSensEnabled() {
                     Section(
                         footer: state.percentageDescription(state.percentage),
@@ -119,7 +132,11 @@ struct AddTempTargetForm: View {
                                 .onChange(of: tempTargetSensitivityAdjustmentType) { _, newValue in
                                     if newValue == .standard {
                                         state.halfBasalTarget = state.settingHalfBasalTarget
-                                        state.percentage = state.computeAdjustedPercentage()
+                                        state.percentage = TempTargetCalculations.computeAdjustedPercentage(
+                                            halfBasalTarget: state.halfBasalTarget,
+                                            target: state.tempTargetTarget,
+                                            autosensMax: state.autosensMax
+                                        )
                                     }
                                 }
                             }
@@ -141,7 +158,10 @@ struct AddTempTargetForm: View {
                                     Text("\(state.computeSliderHigh(), specifier: "%.0f")%")
                                 } onEditingChanged: { editing in
                                     isUsingSlider = editing
-                                    state.halfBasalTarget = Decimal(state.computeHalfBasalTarget())
+                                    state.halfBasalTarget = Decimal(TempTargetCalculations.computeHalfBasalTarget(
+                                        target: state.tempTargetTarget,
+                                        percentage: state.percentage
+                                    ))
                                 }
                                 .listRowSeparator(.hidden, edges: .top)
                             }

+ 5 - 1
Trio/Sources/Modules/Adjustments/View/TempTargets/AdjustmentsRootView+TempTargets.swift

@@ -172,7 +172,11 @@ extension Adjustments.RootView {
                 .RawValue ?? Double(state.settingHalfBasalTarget)
         )
         let percentage = Int(
-            state.computeAdjustedPercentage(usingHBT: tempTargetHalfBasal, usingTarget: tempTargetValue)
+            TempTargetCalculations.computeAdjustedPercentage(
+                halfBasalTarget: tempTargetHalfBasal,
+                target: tempTargetValue,
+                autosensMax: state.autosensMax
+            )
         )
         let remainingTime = tempTarget.date?.timeIntervalSinceNow ?? 0
 

+ 41 - 12
Trio/Sources/Modules/Adjustments/View/TempTargets/EditTempTargetForm.swift

@@ -41,7 +41,11 @@ struct EditTempTargetForm: View {
 
         let H = tempTargetHalfBasal
         let T = tempTargetToEdit.target?.decimalValue ?? 100
-        let calcPercentage = state.computeAdjustedPercentage(usingHBT: H, usingTarget: T)
+        let calcPercentage = TempTargetCalculations.computeAdjustedPercentage(
+            halfBasalTarget: H,
+            target: T,
+            autosensMax: state.autosensMax
+        )
         _percentage = State(initialValue: calcPercentage)
     }
 
@@ -85,7 +89,23 @@ struct EditTempTargetForm: View {
                 }
             }
             .onAppear {
-                if halfBasalTarget != state.settingHalfBasalTarget { tempTargetSensitivityAdjustmentType = .slider }
+                // Determine if this is a custom slider adjustment or a standard with auto-adjustment
+                if halfBasalTarget != nil,
+                   halfBasalTarget != state.settingHalfBasalTarget
+                {
+                    // Check if this was an auto-adjusted standard TT:
+                    // If computeEffectiveHBT with nil stored HBT returns non-nil, settings HBT would produce <= 15%
+                    let isAutoAdjustedStandard = TempTargetCalculations.computeEffectiveHBT(
+                        tempTargetHalfBasalTarget: nil,
+                        settingHalfBasalTarget: state.settingHalfBasalTarget,
+                        target: target,
+                        autosensMax: state.autosensMax
+                    ) != nil
+
+                    if !isAutoAdjustedStandard {
+                        tempTargetSensitivityAdjustmentType = .slider
+                    }
+                }
             }
             .sheet(isPresented: $state.isHelpSheetPresented) {
                 TempTargetHelpView(state: state, helpSheetDetent: $state.helpSheetDetent)
@@ -151,15 +171,19 @@ struct EditTempTargetForm: View {
                     toggleScrollWheel: toggleScrollWheel
                 )
                 .onChange(of: target) {
-                    percentage = state.computeAdjustedPercentage(usingHBT: halfBasalTarget, usingTarget: target)
+                    // percentage = state.computeAdjustedPercentage(usingHBT: halfBasalTarget, usingTarget: target)
+                    // target value changes shall not alter the sensitivity, instead calculate new hbt with sensitivity from slider
+                    halfBasalTarget = Decimal(
+                        TempTargetCalculations
+                            .computeHalfBasalTarget(target: target, percentage: percentage)
+                    )
                 }
             }
             .listRowBackground(Color.chart)
 
-            if target != state.normalTarget {
+            if target != TempTargetCalculations.normalTarget {
                 let computedHalfBasalTarget = Decimal(
-                    state
-                        .computeHalfBasalTarget(usingTarget: target, usingPercentage: percentage)
+                    TempTargetCalculations.computeHalfBasalTarget(target: target, percentage: percentage)
                 )
 
                 if state.isAdjustSensEnabled(usingTarget: target) {
@@ -175,9 +199,10 @@ struct EditTempTargetForm: View {
                                     if newValue == .standard {
                                         halfBasalTarget = nil
                                         hasChanges = true
-                                        percentage = state.computeAdjustedPercentage(
-                                            usingHBT: halfBasalTarget,
-                                            usingTarget: target
+                                        percentage = TempTargetCalculations.computeAdjustedPercentage(
+                                            halfBasalTarget: state.settingHalfBasalTarget,
+                                            target: target,
+                                            autosensMax: state.autosensMax
                                         )
                                     }
                                 }
@@ -198,9 +223,9 @@ struct EditTempTargetForm: View {
                                         set: { newValue in
                                             percentage = newValue
                                             hasChanges = true
-                                            halfBasalTarget = Decimal(state.computeHalfBasalTarget(
-                                                usingTarget: target,
-                                                usingPercentage: percentage
+                                            halfBasalTarget = Decimal(TempTargetCalculations.computeHalfBasalTarget(
+                                                target: target,
+                                                percentage: percentage
                                             ))
                                         }
                                     ),
@@ -303,6 +328,10 @@ struct EditTempTargetForm: View {
             Spacer()
             Button(action: {
                 saveChanges()
+                debug(
+                    .default,
+                    "TempTarget: target=\(target), HBT=\(state.settingHalfBasalTarget), effectiveHBT=\(String(describing: halfBasalTarget)), percentage=\(Int(percentage))%, adjustmentType=\(tempTargetSensitivityAdjustmentType.rawValue)"
+                )
                 do {
                     guard let moc = tempTarget.managedObjectContext else { return }
                     guard moc.hasChanges else { return }

+ 39 - 14
Trio/Sources/Modules/Adjustments/View/TempTargets/TempTargetHelpView.swift

@@ -7,20 +7,45 @@ struct TempTargetHelpView: View {
     var body: some View {
         NavigationStack {
             List {
-                VStack(alignment: .leading, spacing: 10) {
-                    Text(
-                        "A Temporary Target replaces the current Target Glucose specified in Therapy settings."
-                    )
-                    Text(
-                        "Depending on your Target Behavior settings (see Settings > the Algorithm > Target Behavior), these temporary glucose targets can also raise Insulin Sensitivity for high targets or lower sensitivity for low targets."
-                    )
-                    Text(
-                        "Furthermore, you could adjust that sensitivity change independently from the Half Basal Exercise Target specified in Algorithm > Target Behavior settings by deliberatly setting a customized Insulin Percentage for a Temp Target."
-                    )
-                    Text(
-                        "A pre-condition to have Temp Targets adjust Sensitivity is that the respective Target Behavior settings High Temp Target Raises Sensitivity or Low Temp Target Lowers Sensitivity are set to enabled."
-                    )
-                }.listRowBackground(Color.gray.opacity(0.1))
+                Section {
+                    VStack(alignment: .leading, spacing: 10) {
+                        Text(
+                            "A Temporary Target replaces the current Target Glucose specified in Therapy settings."
+                        )
+                        Text(
+                            "Depending on your Target Behavior settings (see Settings > the Algorithm > Target Behavior), these temporary glucose targets can also raise Insulin Sensitivity for high targets or lower sensitivity for low targets."
+                        )
+                        Text(
+                            "Furthermore, you could adjust that sensitivity change independently from the Half Basal Exercise Target specified in Algorithm > Target Behavior settings by deliberately setting a customized Insulin Percentage for a Temp Target."
+                        )
+                        Text(
+                            "A pre-condition to have Temp Targets adjust Sensitivity is that the respective Target Behavior settings High Temp Target Raises Sensitivity or Low Temp Target Lowers Sensitivity are set to enabled."
+                        )
+                    }
+                } header: {
+                    Text("Overview")
+                }
+                .listRowBackground(Color.gray.opacity(0.1))
+
+                Section {
+                    VStack(alignment: .leading, spacing: 10) {
+                        Text(
+                            "Sensitivity changes from Temp Targets have a built-in minimum of 15%. Even very high Temp Targets cannot reduce insulin delivery below 15% of normal."
+                        )
+                        Text(
+                            "This 15% minimum is a safety limit taken from oref (OpenAPS reference design) and AndroidAPS. It helps prevent insulin delivery from dropping to unsafe levels."
+                        )
+                        Text(
+                            "Important: Autosens Min and Autosens Max do not affect Temp Targets in the same way. Autosens Max limits how much insulin can be increased, but Autosens Min does not remove the 15% minimum when insulin is reduced."
+                        )
+                        Text(
+                            "This difference exists because situations like exercise often need a much larger insulin reduction than Autosens would detect during a normal daily routine."
+                        )
+                    }
+                } header: {
+                    Text("Sensitivity Limits")
+                }
+                .listRowBackground(Color.gray.opacity(0.1))
             }
             .navigationBarTitle("Help", displayMode: .inline)
 

+ 0 - 11
Trio/Sources/Modules/Home/HomeStateModel+Setup/TempTargetSetup.swift

@@ -118,15 +118,4 @@ extension Home.StateModel {
             debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save Temp Target with error: \(error)")
         }
     }
-
-    func computeAdjustedPercentage(halfBasalTargetValue: Decimal, tempTargetValue: Decimal) -> Int {
-        let normalTarget: Decimal = 100
-        let deviationFromNormal = halfBasalTargetValue - normalTarget
-
-        let adjustmentFactor = deviationFromNormal + (tempTargetValue - normalTarget)
-        let adjustmentRatio: Decimal = (deviationFromNormal * adjustmentFactor <= 0) ? autosensMax : deviationFromNormal /
-            adjustmentFactor
-
-        return Int(Double(min(adjustmentRatio, autosensMax) * 100).rounded())
-    }
 }

+ 9 - 5
Trio/Sources/Modules/Home/View/HomeRootView.swift

@@ -284,16 +284,20 @@ extension Home {
             var durationString = ""
             var percentageString = ""
             var target = (latestTempTarget.target ?? 100) as Decimal
-            var halfBasalTarget: Decimal = 160
-            if latestTempTarget.halfBasalTarget != nil {
-                halfBasalTarget = latestTempTarget.halfBasalTarget! as Decimal
-            } else { halfBasalTarget = state.settingHalfBasalTarget }
+            // Use TempTargetCalculations to get effective HBT (handles both custom and auto-adjusted standard TT)
+            let effectiveHBT = TempTargetCalculations.computeEffectiveHBT(
+                tempTargetHalfBasalTarget: latestTempTarget.halfBasalTarget?.decimalValue,
+                settingHalfBasalTarget: state.settingHalfBasalTarget,
+                target: target,
+                autosensMax: state.autosensMax
+            ) ?? state.settingHalfBasalTarget
             var showPercentage = false
             if target > 100, state.isExerciseModeActive || state.highTTraisesSens { showPercentage = true }
             if target < 100, state.lowTTlowersSens, state.autosensMax > 1 { showPercentage = true }
             if showPercentage {
                 percentageString =
-                    " \(state.computeAdjustedPercentage(halfBasalTargetValue: halfBasalTarget, tempTargetValue: target))%" }
+                    " \(Int(TempTargetCalculations.computeAdjustedPercentage(halfBasalTarget: effectiveHBT, target: target, autosensMax: state.autosensMax)))%"
+            }
             target = state.units == .mmolL ? target.asMmolL : target
             let targetString = target == 0 ? "" : (fetchedTargetFormatter.string(from: target as NSNumber) ?? "") + " " +
                 state.units.rawValue + percentageString