Jon B Mårtensson преди 3 години
родител
ревизия
d5e6626781
променени са 31 файла, в които са добавени 538 реда и са изтрити 162 реда
  1. 1 1
      Config.xcconfig
  2. 2 1
      Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents
  3. 1 1
      Dependencies/CGMBLEKit/CGMBLEKitUI/it.lproj/Localizable.strings
  4. 3 2
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Bluetooth/LibreTransmitterMetadata.swift
  5. 2 2
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreSensor/SensorContents/PreLibre2.swift
  6. 1 1
      FreeAPS/Resources/javascript/bundle/determine-basal.js
  7. 7 7
      FreeAPS/Sources/APS/CGM/CGMType.swift
  8. 4 3
      FreeAPS/Sources/APS/Storage/GlucoseStorage.swift
  9. 41 1
      FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings
  10. 3 3
      FreeAPS/Sources/Localizations/Main/it.lproj/Localizable.strings
  11. 41 0
      FreeAPS/Sources/Localizations/Main/sv.lproj/Localizable.strings
  12. 4 0
      FreeAPS/Sources/Models/Suggestion.swift
  13. 3 1
      FreeAPS/Sources/Modules/AddCarbs/AddCarbsStateModel.swift
  14. 0 1
      FreeAPS/Sources/Modules/AddTempTarget/AddTempTargetStateModel.swift
  15. 1 2
      FreeAPS/Sources/Modules/AddTempTarget/View/AddTempTargetRootView.swift
  16. 30 4
      FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift
  17. 133 16
      FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift
  18. 4 4
      FreeAPS/Sources/Modules/CGM/View/CGMSetupView.swift
  19. 46 0
      FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift
  20. 37 1
      FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift
  21. 3 2
      FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift
  22. 1 1
      FreeAPS/Sources/Modules/Home/View/HomeRootView.swift
  23. 125 38
      FreeAPS/Sources/Modules/Stat/View/ChartsView.swift
  24. 23 33
      FreeAPS/Sources/Modules/Stat/View/StatsView.swift
  25. 2 2
      FreeAPS/Sources/Modules/StatConfig/StatConfigStateModel.swift
  26. 2 2
      FreeAPS/Sources/Modules/TargetsEditor/TargetsEditorDataFlow.swift
  27. 3 3
      FreeAPS/Sources/Modules/TargetsEditor/TargetsEditorStateModel.swift
  28. 1 17
      FreeAPS/Sources/Modules/TargetsEditor/View/TargetsEditorRootView.swift
  29. 3 3
      FreeAPS/Sources/Router/Screen.swift
  30. 8 2
      FreeAPS/Sources/Services/WatchManager/WatchManager.swift
  31. 3 8
      FreeAPSWatch WatchKit Extension/Views/MainView.swift

+ 1 - 1
Config.xcconfig

@@ -1,5 +1,5 @@
 APP_DISPLAY_NAME = iAPS
 APP_DISPLAY_NAME = iAPS
-APP_VERSION = 2.1.0
+APP_VERSION = 2.1.9
 APP_BUILD_NUMBER = 1
 APP_BUILD_NUMBER = 1
 COPYRIGHT_NOTICE =
 COPYRIGHT_NOTICE =
 DEVELOPER_TEAM = ##TEAM_ID##
 DEVELOPER_TEAM = ##TEAM_ID##

+ 2 - 1
Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21754" systemVersion="22E261" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
+<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22158.8" systemVersion="22F66" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
     <entity name="BGaverages" representedClassName="BGaverages" syncable="YES" codeGenerationType="class">
     <entity name="BGaverages" representedClassName="BGaverages" syncable="YES" codeGenerationType="class">
         <attribute name="average" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
         <attribute name="average" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
         <attribute name="average_1" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
         <attribute name="average_1" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
@@ -92,6 +92,7 @@
     <entity name="Readings" representedClassName="Readings" syncable="YES" codeGenerationType="class">
     <entity name="Readings" representedClassName="Readings" syncable="YES" codeGenerationType="class">
         <attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
         <attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
         <attribute name="glucose" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
         <attribute name="glucose" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
+        <attribute name="id" optional="YES" attributeType="String"/>
     </entity>
     </entity>
     <entity name="StatsData" representedClassName="StatsData" syncable="YES" codeGenerationType="class">
     <entity name="StatsData" representedClassName="StatsData" syncable="YES" codeGenerationType="class">
         <attribute name="lastrun" attributeType="Date" defaultDateTimeInterval="704497620" usesScalarValueType="NO"/>
         <attribute name="lastrun" attributeType="Date" defaultDateTimeInterval="704497620" usesScalarValueType="NO"/>

+ 1 - 1
Dependencies/CGMBLEKit/CGMBLEKitUI/it.lproj/Localizable.strings

@@ -1,5 +1,5 @@
 /* Format string for glucose trend per minute. (1: glucose value and unit) */
 /* Format string for glucose trend per minute. (1: glucose value and unit) */
-"%@/min" = "%@/min";
+"%@/min" = "%@/minuto";
 
 
 /* Confirmation message for deleting a CGM */
 /* Confirmation message for deleting a CGM */
 "Are you sure you want to delete this CGM?" = "Sei sicuro di voler eliminare questo CGM?";
 "Are you sure you want to delete this CGM?" = "Sei sicuro di voler eliminare questo CGM?";

+ 3 - 2
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Bluetooth/LibreTransmitterMetadata.swift

@@ -80,6 +80,7 @@ public enum SensorType: String, CustomStringConvertible {
     case libre1    = "DF"
     case libre1    = "DF"
     case libre1A2 =  "A2"
     case libre1A2 =  "A2"
     case libre2    = "9D"
     case libre2    = "9D"
+    case libre2C5 = "C5"
     case libreUS14day   = "E5"
     case libreUS14day   = "E5"
     case libreUS14dayE6 = "E6"
     case libreUS14dayE6 = "E6"
     case libreProH = "70"
     case libreProH = "70"
@@ -90,7 +91,7 @@ public enum SensorType: String, CustomStringConvertible {
             return "Libre 1"
             return "Libre 1"
         case .libre1A2:
         case .libre1A2:
             return "Libre 1 A2"
             return "Libre 1 A2"
-        case .libre2:
+        case .libre2, .libre2C5:
             return "Libre 2"
             return "Libre 2"
         case .libreUS14day, .libreUS14dayE6:
         case .libreUS14day, .libreUS14dayE6:
             return "Libre US"
             return "Libre US"
@@ -106,7 +107,7 @@ public extension SensorType {
 
 
         let start = patchInfo[0..<2].uppercased()
         let start = patchInfo[0..<2].uppercased()
 
 
-        let choices: [String: SensorType] = ["DF": .libre1, "A2": .libre1A2, "9D": .libre2, "E5": .libreUS14day, "E6": .libreUS14dayE6, "70": .libreProH]
+        let choices: [String: SensorType] = ["DF": .libre1, "A2": .libre1A2, "9D": .libre2, "C5": .libre2, "E5": .libreUS14day, "E6": .libreUS14dayE6, "70": .libreProH]
 
 
         if let res = choices[start] {
         if let res = choices[start] {
             self = res
             self = res

+ 2 - 2
Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreSensor/SensorContents/PreLibre2.swift

@@ -9,7 +9,7 @@ public enum Libre2 {
     ///   - data: Encrypted FRAM data
     ///   - data: Encrypted FRAM data
     /// - Returns: Decrypted FRAM data
     /// - Returns: Decrypted FRAM data
     static public func decryptFRAM(type: SensorType, id: [UInt8], info: [UInt8], data: [UInt8]) throws -> [UInt8] {
     static public func decryptFRAM(type: SensorType, id: [UInt8], info: [UInt8], data: [UInt8]) throws -> [UInt8] {
-        guard type == .libre2 || type == .libreUS14day || type == .libreUS14dayE6 else {
+        guard type == .libre2 || type == .libre2C5 || type == .libreUS14day || type == .libreUS14dayE6 else {
             struct DecryptFRAMError: Error {
             struct DecryptFRAMError: Error {
                 let errorDescription = "Unsupported sensor type"
                 let errorDescription = "Unsupported sensor type"
             }
             }
@@ -24,7 +24,7 @@ public enum Libre2 {
                     return 0xcadc
                     return 0xcadc
                 }
                 }
                 return UInt16(info[5], info[4])
                 return UInt16(info[5], info[4])
-            case .libre2:
+            case .libre2, .libre2C5:
                 return UInt16(info[5], info[4]) ^ 0x44
                 return UInt16(info[5], info[4]) ^ 0x44
             default: fatalError("Unsupported sensor type")
             default: fatalError("Unsupported sensor type")
             }
             }

Файловите разлики са ограничени, защото са твърде много
+ 1 - 1
FreeAPS/Resources/javascript/bundle/determine-basal.js


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

@@ -5,8 +5,8 @@ enum CGMType: String, JSON, CaseIterable, Identifiable {
 
 
     case nightscout
     case nightscout
     case xdrip
     case xdrip
-    case dexcomG6
     case dexcomG5
     case dexcomG5
+    case dexcomG6
     case dexcomG7
     case dexcomG7
     case simulator
     case simulator
     case libreTransmitter
     case libreTransmitter
@@ -21,10 +21,10 @@ enum CGMType: String, JSON, CaseIterable, Identifiable {
             return "xDrip"
             return "xDrip"
         case .glucoseDirect:
         case .glucoseDirect:
             return "Glucose Direct"
             return "Glucose Direct"
-        case .dexcomG6:
-            return "Dexcom G6"
         case .dexcomG5:
         case .dexcomG5:
             return "Dexcom G5"
             return "Dexcom G5"
+        case .dexcomG6:
+            return "Dexcom G6"
         case .dexcomG7:
         case .dexcomG7:
             return "Dexcom G7"
             return "Dexcom G7"
         case .simulator:
         case .simulator:
@@ -45,12 +45,12 @@ enum CGMType: String, JSON, CaseIterable, Identifiable {
             return URL(string: "xdripswift://")!
             return URL(string: "xdripswift://")!
         case .glucoseDirect:
         case .glucoseDirect:
             return URL(string: "libredirect://")!
             return URL(string: "libredirect://")!
+        case .dexcomG5:
+            return URL(string: "dexcomgcgm://")!
         case .dexcomG6:
         case .dexcomG6:
             return URL(string: "dexcomg6://")!
             return URL(string: "dexcomg6://")!
         case .dexcomG7:
         case .dexcomG7:
             return URL(string: "dexcomg7://")!
             return URL(string: "dexcomg7://")!
-        case .dexcomG5:
-            return URL(string: "dexcomgcgm://")!
         case .simulator:
         case .simulator:
             return nil
             return nil
         case .libreTransmitter:
         case .libreTransmitter:
@@ -77,10 +77,10 @@ enum CGMType: String, JSON, CaseIterable, Identifiable {
                 "Using shared app group with external CGM app xDrip4iOS",
                 "Using shared app group with external CGM app xDrip4iOS",
                 comment: "Shared app group xDrip4iOS"
                 comment: "Shared app group xDrip4iOS"
             )
             )
-        case .dexcomG6:
-            return NSLocalizedString("Dexcom G6 app", comment: "Dexcom G6 app")
         case .dexcomG5:
         case .dexcomG5:
             return NSLocalizedString("Native G5 app", comment: "Native G5 app")
             return NSLocalizedString("Native G5 app", comment: "Native G5 app")
+        case .dexcomG6:
+            return NSLocalizedString("Dexcom G6 app", comment: "Dexcom G6 app")
         case .dexcomG7:
         case .dexcomG7:
             return NSLocalizedString("Dexcom G7 app", comment: "Dexcom G76 app")
             return NSLocalizedString("Dexcom G7 app", comment: "Dexcom G76 app")
         case .simulator:
         case .simulator:

+ 4 - 3
FreeAPS/Sources/APS/Storage/GlucoseStorage.swift

@@ -56,23 +56,24 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                     }
                     }
                 }
                 }
 
 
-                // MARK: Save to CoreData. TEST
+                // MARK: Save to CoreData.
 
 
                 var bg_ = 0
                 var bg_ = 0
                 var bgDate = Date()
                 var bgDate = Date()
+                var id = ""
 
 
                 if glucose.isNotEmpty {
                 if glucose.isNotEmpty {
                     bg_ = glucose[0].glucose ?? 0
                     bg_ = glucose[0].glucose ?? 0
                     bgDate = glucose[0].dateString
                     bgDate = glucose[0].dateString
+                    id = glucose[0].id
                 }
                 }
 
 
                 if bg_ != 0 {
                 if bg_ != 0 {
                     self.coredataContext.perform {
                     self.coredataContext.perform {
                         let dataForForStats = Readings(context: self.coredataContext)
                         let dataForForStats = Readings(context: self.coredataContext)
-
                         dataForForStats.date = bgDate
                         dataForForStats.date = bgDate
                         dataForForStats.glucose = Int16(bg_)
                         dataForForStats.glucose = Int16(bg_)
-
+                        dataForForStats.id = id
                         try? self.coredataContext.save()
                         try? self.coredataContext.save()
                     }
                     }
                 }
                 }

+ 41 - 1
FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings

@@ -1165,7 +1165,6 @@ Enact a temp Basal or a temp target */
 /* */
 /* */
 "Normal " = "Normal ";
 "Normal " = "Normal ";
 
 
-/* */
 "Currently no Override active" = "Currently no Override active";
 "Currently no Override active" = "Currently no Override active";
 
 
 /* */
 /* */
@@ -1279,6 +1278,47 @@ Enact a temp Basal or a temp target */
 /* */
 /* */
 "Save as Preset" = "Save as Preset";
 "Save as Preset" = "Save as Preset";
 
 
+/* ----------------------- New Bolus Calculator ---------------------------*/
+
+/* Warning about bolus recommendation. Title */
+"Warning!" = "Warning!";
+
+/* Alert to confirm bolus amount to add */
+"\n\nTap 'Add' to continue with selected amount." = "\n\nTap 'Add' to continue with selected amount.";
+
+/* */
+"Eventual Glucose" = "Eventual Glucose";
+
+/* */
+"Target Glucose" = "Target Glucose";
+
+/* */
+"Percentage setting" = "Percentage setting";
+
+/* */
+"Insulin Sensitivity" = "Insulin Sensitivity";
+
+/* Formuala displayed in Bolus info pop-up*/
+"(Eventual Glucose - Target) / ISF =" = "(Eventual Glucose - Target) / ISF =";
+
+/* Bolus pop-up footer */
+"Carbs and previous insulin are included in the glucose prediction, but if the Eventual Glucose is lower than the Target Glucose, a bolus will not be recommended." = "Carbs and previous insulin are included in the glucose prediction, but if the Eventual Glucose is lower than the Target Glucose, a bolus will not be recommended.";
+
+/* Hide pop-up */
+"Hide" = "Hide";
+
+/* Error string 1. Make translation very short! */
+"Predicted Glucose, %@ %@, is below threshold (%@!)" = "Predicted Glucose, %@ %@, is below threshold (%@!)";
+
+/* Error string 2. Make translation very short! */
+"Eventual Glucose > Target Glucose, but glucose is falling faster than expected. Expected: %@. Falling: %@" = "Eventual Glucose > Target Glucose, but glucose is falling faster than expected. Expected: %@. Falling: %@";
+
+/* Error string 3. Make translation very short! */
+"Minimum predicted Glucose is %@ %@" = "Minimum predicted Glucose is %@ %@";
+
+/* Error string 4. Make translation very short! */
+"Predicted Glucose %@ %@, is below Threshold of %@" = "Predicted Glucose %@ %@, is below Threshold of %@";
+
 /* -------------------------------------------------------------------------------------------
 /* -------------------------------------------------------------------------------------------
   DASH strings
   DASH strings
 */
 */

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

@@ -113,7 +113,7 @@
 "Enact Temp Target" = "Target Temporaneo";
 "Enact Temp Target" = "Target Temporaneo";
 
 
 /* */
 /* */
-"Target" = "Target";
+"Target" = "Obiettivo";
 
 
 /* */
 /* */
 "Basal Insulin and Sensitivity ratio" = "Rapporto basale insulina e sensibilità";
 "Basal Insulin and Sensitivity ratio" = "Rapporto basale insulina e sensibilità";
@@ -558,7 +558,7 @@ Enact a temp Basal or a temp target */
 "Meter glucose" = "Inserisci glicemia";
 "Meter glucose" = "Inserisci glicemia";
 
 
 /* */
 /* */
-"Info" = "Info";
+"Info" = "Informazioni";
 
 
 /*v*/
 /*v*/
 "Slope" = "Pendenza";
 "Slope" = "Pendenza";
@@ -1091,7 +1091,7 @@ Enact a temp Basal or a temp target */
 
 
 /* New ALerts ------------------------- */
 /* New ALerts ------------------------- */
 /* Info title */
 /* Info title */
-"Info" = "Info";
+"Info" = "Informazioni";
 
 
 /* Warning title */
 /* Warning title */
 "Warning" = "Avvertimento";
 "Warning" = "Avvertimento";

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

@@ -1278,6 +1278,47 @@ Enact a temp Basal or a temp target */
 /* */
 /* */
 "Save as Preset" = "Spara som förval";
 "Save as Preset" = "Spara som förval";
 
 
+/* ----------------------- New Bolus Calculator ---------------------------*/
+
+/* Warning about bolus recommendation. Title */
+"Warning!" = "Varning!";
+
+/* Alert to confirm bolus amount to add */
+"\n\nTap 'Add' to continue with selected amount." = "\n\nTryck 'Lägg till' för att fylla i bolusmängd.";
+
+/* */
+"Eventual Glucose" = "Blodsockerprognos";
+
+/* */
+"Target Glucose" = "Målvärde";
+
+/* */
+"Percentage setting" = "Bolusprocentinställning";
+
+/* */
+"Insulin Sensitivity" = "Insulinkänslighet";
+
+/* Formuala displayed in Bolus info pop-up. Make translation short! */
+"(Eventual Glucose - Target) / ISF =" = "(Blodsockerprognos - Målvärde) / ISF =";
+
+/* Bolus pop-up footer */
+"Carbs and previous insulin are included in the glucose prediction, but if the Eventual Glucose is lower than the Target Glucose, a bolus will not be recommended." = "Kolhydrater och tidigare insulin ingår i prognos, men om prognos är lägre än målvärde kommer ingen bolus att föreslås.";
+
+/* Hide pop-up */
+"Hide" = "Göm";
+
+/* Error string 1. Make translation very short! */
+"Predicted Glucose, %@ %@, is below threshold (%@!)" = "Blodsockerprognos, %@ %@, är lägre än (%@!)";
+
+/* Error string 2. Make translation very short! */
+"Eventual Glucose > Target Glucose, but glucose is falling faster than expected. Expected: %@. Falling: %@" = "Blodsockerprognos > målvärde, med blodsockret sjunker snabbare än väntat. Förväntat: %@. Sjunker: %@";
+
+/* Error string 3. Make translation very short! */
+"Minimum predicted Glucose is %@ %@" = "Lägsta blodsockerprognos är %@ %@!";
+
+/* Error string 4. Make translation very short! */
+"Predicted Glucose %@ %@, is below Threshold of %@" = "Blodsockerprognos %@ %@, är under tröskelvärde %@";
+
 /* -------------------------------------------------------------------------------------------
 /* -------------------------------------------------------------------------------------------
   DASH strings
   DASH strings
 */
 */

+ 4 - 0
FreeAPS/Sources/Models/Suggestion.swift

@@ -22,6 +22,8 @@ struct Suggestion: JSON, Equatable {
     let tdd: Decimal?
     let tdd: Decimal?
     let insulin: Insulin?
     let insulin: Insulin?
     let current_target: Decimal?
     let current_target: Decimal?
+    let insulinForManualBolus: Decimal?
+    let manualBolusErrorString: String?
 }
 }
 
 
 struct Predictions: JSON, Equatable {
 struct Predictions: JSON, Equatable {
@@ -61,6 +63,8 @@ extension Suggestion {
         case tdd = "TDD"
         case tdd = "TDD"
         case insulin
         case insulin
         case current_target
         case current_target
+        case insulinForManualBolus
+        case manualBolusErrorString
     }
     }
 }
 }
 
 

+ 3 - 1
FreeAPS/Sources/Modules/AddCarbs/AddCarbsStateModel.swift

@@ -16,6 +16,7 @@ extension AddCarbs {
         @Published var dish: String = ""
         @Published var dish: String = ""
         @Published var selection: Presets?
         @Published var selection: Presets?
         @Published var summation: [String] = []
         @Published var summation: [String] = []
+        @Published var manualBolus: Bool = false
 
 
         let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
         let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
 
 
@@ -108,7 +109,8 @@ extension AddCarbs {
                 apsManager.determineBasalSync()
                 apsManager.determineBasalSync()
                 showModal(for: nil)
                 showModal(for: nil)
             } else {
             } else {
-                showModal(for: .bolus(waitForSuggestion: true))
+                manualBolus.toggle()
+                showModal(for: .bolus(waitForSuggestion: true, manualBolus: manualBolus))
             }
             }
         }
         }
 
 

+ 0 - 1
FreeAPS/Sources/Modules/AddTempTarget/AddTempTargetStateModel.swift

@@ -7,7 +7,6 @@ extension AddTempTarget {
         @Injected() var apsManager: APSManager!
         @Injected() var apsManager: APSManager!
 
 
         let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
         let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
-        @Environment(\.managedObjectContext) var moc
 
 
         @Published var low: Decimal = 0
         @Published var low: Decimal = 0
         // @Published var target: Decimal = 0
         // @Published var target: Decimal = 0

+ 1 - 2
FreeAPS/Sources/Modules/AddTempTarget/View/AddTempTargetRootView.swift

@@ -16,8 +16,6 @@ extension AddTempTarget {
             sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)]
             sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)]
         ) var isEnabledArray: FetchedResults<TempTargetsSlider>
         ) var isEnabledArray: FetchedResults<TempTargetsSlider>
 
 
-        @Environment(\.managedObjectContext) var moc
-
         private var formatter: NumberFormatter {
         private var formatter: NumberFormatter {
             let formatter = NumberFormatter()
             let formatter = NumberFormatter()
             formatter.numberStyle = .decimal
             formatter.numberStyle = .decimal
@@ -116,6 +114,7 @@ extension AddTempTarget {
                         DatePicker("Date", selection: $state.date)
                         DatePicker("Date", selection: $state.date)
                         Button { isPromtPresented = true }
                         Button { isPromtPresented = true }
                         label: { Text("Save as preset") }
                         label: { Text("Save as preset") }
+                            .disabled(state.duration == 0)
                     }
                     }
                 }
                 }
 
 

+ 30 - 4
FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift

@@ -11,11 +11,25 @@ extension Bolus {
         @Published var insulinRecommended: Decimal = 0
         @Published var insulinRecommended: Decimal = 0
         @Published var insulinRequired: Decimal = 0
         @Published var insulinRequired: Decimal = 0
         @Published var waitForSuggestion: Bool = false
         @Published var waitForSuggestion: Bool = false
+        @Published var manual: Bool = false
+        @Published var error: Bool = false
+        @Published var errorString: String = ""
+
+        @Published var evBG: Int = 0
+        @Published var insulin: Decimal = 0
+        @Published var target: Decimal = 0
+        @Published var isf: Decimal = 0
+        @Published var percentage: Decimal = 0
+
+        @Published var units: GlucoseUnits = .mmolL
+
         var waitForSuggestionInitial: Bool = false
         var waitForSuggestionInitial: Bool = false
 
 
         override func subscribe() {
         override func subscribe() {
             setupInsulinRequired()
             setupInsulinRequired()
             broadcaster.register(SuggestionObserver.self, observer: self)
             broadcaster.register(SuggestionObserver.self, observer: self)
+            units = settingsManager.settings.units
+            percentage = settingsManager.settings.insulinReqPercentage
 
 
             if waitForSuggestionInitial {
             if waitForSuggestionInitial {
                 apsManager.determineBasal()
                 apsManager.determineBasal()
@@ -75,11 +89,23 @@ extension Bolus {
         func setupInsulinRequired() {
         func setupInsulinRequired() {
             DispatchQueue.main.async {
             DispatchQueue.main.async {
                 self.insulinRequired = self.provider.suggestion?.insulinReq ?? 0
                 self.insulinRequired = self.provider.suggestion?.insulinReq ?? 0
+
+                // Manual Bolus recommendation screen after a carb entry (normally) yields a higher amount than the insulin reqiured amount computed for SMBs (auto boluses). Carbs combined with a manual bolus threfore now (test) uses the Eventual BG for glucose prediction, whereas the insulinReg for SMBs uses the minPredBG for glucose prediction (typically lower than Eventual BG).
+
+                self.evBG = self.provider.suggestion?.eventualBG ?? 0
+                self.insulin = self.provider.suggestion?.insulinForManualBolus ?? 0
+                self.target = self.provider.suggestion?.current_target ?? 0
+                self.isf = self.provider.suggestion?.isf ?? 0
+
+                if self.settingsManager.settings.insulinReqPercentage != 100 {
+                    self.insulinRecommended = self.insulin * (self.settingsManager.settings.insulinReqPercentage / 100)
+                } else { self.insulinRecommended = self.insulin }
+
+                self.errorString = self.provider.suggestion?.manualBolusErrorString ?? ""
+                if self.errorString.count > 8 { self.error = true }
+
                 self.insulinRecommended = self.apsManager
                 self.insulinRecommended = self.apsManager
-                    .roundBolus(amount: max(
-                        self.insulinRequired * (self.settingsManager.settings.insulinReqPercentage / 100) * 2,
-                        0
-                    ))
+                    .roundBolus(amount: max(self.insulinRecommended, 0))
             }
             }
         }
         }
     }
     }

+ 133 - 16
FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift

@@ -5,8 +5,13 @@ extension Bolus {
     struct RootView: BaseView {
     struct RootView: BaseView {
         let resolver: Resolver
         let resolver: Resolver
         let waitForSuggestion: Bool
         let waitForSuggestion: Bool
+        let manualBolus: Bool
         @StateObject var state = StateModel()
         @StateObject var state = StateModel()
         @State private var isAddInsulinAlertPresented = false
         @State private var isAddInsulinAlertPresented = false
+        @State private var presentInfo = false
+        @State private var displayError = false
+
+        @Environment(\.colorScheme) var colorScheme
 
 
         private var formatter: NumberFormatter {
         private var formatter: NumberFormatter {
             let formatter = NumberFormatter()
             let formatter = NumberFormatter()
@@ -17,7 +22,7 @@ extension Bolus {
 
 
         var body: some View {
         var body: some View {
             Form {
             Form {
-                Section(header: Text("Recommendation")) {
+                Section {
                     if state.waitForSuggestion {
                     if state.waitForSuggestion {
                         HStack {
                         HStack {
                             Text("Wait please").foregroundColor(.secondary)
                             Text("Wait please").foregroundColor(.secondary)
@@ -26,34 +31,31 @@ extension Bolus {
                         }
                         }
                     } else {
                     } else {
                         HStack {
                         HStack {
-                            Text("Insulin required").foregroundColor(.secondary)
-                            Spacer()
-                            Text(
-                                formatter
-                                    .string(from: state.insulinRequired as NSNumber)! +
-                                    NSLocalizedString(" U", comment: "Insulin unit")
-                            ).foregroundColor(.secondary)
-                        }.contentShape(Rectangle())
-                            .onTapGesture {
-                                state.amount = state.insulinRequired
-                            }
-                        HStack {
                             Text("Insulin recommended")
                             Text("Insulin recommended")
                             Spacer()
                             Spacer()
                             Text(
                             Text(
                                 formatter
                                 formatter
                                     .string(from: state.insulinRecommended as NSNumber)! +
                                     .string(from: state.insulinRecommended as NSNumber)! +
                                     NSLocalizedString(" U", comment: "Insulin unit")
                                     NSLocalizedString(" U", comment: "Insulin unit")
-                            ).foregroundColor(.secondary)
+                            ).foregroundColor((state.error && state.insulinRecommended > 0) ? .red : .secondary)
                         }.contentShape(Rectangle())
                         }.contentShape(Rectangle())
                             .onTapGesture {
                             .onTapGesture {
-                                state.amount = state.insulinRecommended
+                                if state.error, state.insulinRecommended > 0 { displayError = true }
+                                else { state.amount = state.insulinRecommended }
                             }
                             }
+                        HStack {
+                            Image(systemName: "info.bubble").symbolRenderingMode(.palette).foregroundStyle(
+                                .primary, .blue
+                            )
+                        }.onTapGesture {
+                            presentInfo.toggle()
+                        }
                     }
                     }
                 }
                 }
+                header: { Text("Recommendation") }
 
 
                 if !state.waitForSuggestion {
                 if !state.waitForSuggestion {
-                    Section(header: Text("Bolus")) {
+                    Section {
                         HStack {
                         HStack {
                             Text("Amount")
                             Text("Amount")
                             Spacer()
                             Spacer()
@@ -67,6 +69,7 @@ extension Bolus {
                             Text("U").foregroundColor(.secondary)
                             Text("U").foregroundColor(.secondary)
                         }
                         }
                     }
                     }
+                    header: { Text("Bolus") }
 
 
                     Section {
                     Section {
                         Button { state.add() }
                         Button { state.add() }
@@ -99,15 +102,129 @@ extension Bolus {
                     secondaryButton: .cancel()
                     secondaryButton: .cancel()
                 )
                 )
             }
             }
+            .alert(isPresented: $displayError) {
+                Alert(
+                    title: Text("Warning!"),
+                    message: Text("\n" + NSLocalizedString(state.errorString, comment: "") + NSLocalizedString(
+                        "\n\nTap 'Add' to continue with selected amount.",
+                        comment: "Alert text to confirm bolus amount to add"
+                    )),
+                    primaryButton: .destructive(
+                        Text("Add"),
+                        action: {
+                            state.amount = state.insulinRecommended
+                            displayError = false
+                        }
+                    ),
+                    secondaryButton: .cancel()
+                )
+            }
             .onAppear {
             .onAppear {
                 configureView {
                 configureView {
                     state.waitForSuggestionInitial = waitForSuggestion
                     state.waitForSuggestionInitial = waitForSuggestion
                     state.waitForSuggestion = waitForSuggestion
                     state.waitForSuggestion = waitForSuggestion
+                    state.manual = manualBolus
                 }
                 }
             }
             }
             .navigationTitle("Enact Bolus")
             .navigationTitle("Enact Bolus")
             .navigationBarTitleDisplayMode(.automatic)
             .navigationBarTitleDisplayMode(.automatic)
             .navigationBarItems(leading: Button("Close", action: state.hideModal))
             .navigationBarItems(leading: Button("Close", action: state.hideModal))
+            .popup(isPresented: presentInfo, alignment: .center, direction: .bottom) {
+                bolusInfo
+            }
+        }
+
+        var bolusInfo: some View {
+            VStack {
+                // Variables
+                VStack(spacing: 3) {
+                    HStack {
+                        Text("Eventual Glucose").foregroundColor(.secondary)
+                        let evg = state.units == .mmolL ? Decimal(state.evBG).asMmolL : Decimal(state.evBG)
+                        let fractionDigit = state.units == .mmolL ? 1 : 0
+                        Text(evg.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigit))))
+                        Text(state.units.rawValue).foregroundColor(.secondary)
+                    }
+                    HStack {
+                        Text("Target Glucose").foregroundColor(.secondary)
+                        let target = state.units == .mmolL ? state.target.asMmolL : state.target
+                        let fractionDigit = state.units == .mmolL ? 1 : 0
+                        Text(target.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigit))))
+                        Text(state.units.rawValue).foregroundColor(.secondary)
+                    }
+                    HStack {
+                        Text("ISF").foregroundColor(.secondary)
+                        let isf = state.isf
+                        Text(isf.formatted())
+                        Text(state.units.rawValue + NSLocalizedString("/U", comment: "/Insulin unit"))
+                            .foregroundColor(.secondary)
+                    }
+                    HStack {
+                        Text("ISF:")
+                        Text("Insulin Sensitivity")
+                    }.foregroundColor(.secondary).italic()
+                    if state.percentage != 100 {
+                        HStack {
+                            Text("Percentage setting").foregroundColor(.secondary)
+                            let percentage = state.percentage
+                            Text(percentage.formatted())
+                            Text("%").foregroundColor(.secondary)
+                        }
+                    }
+                }
+                .font(.footnote)
+                .padding(.top, 10)
+                Divider()
+                // Formula
+                VStack(spacing: 5) {
+                    let unit = NSLocalizedString(
+                        " U",
+                        comment: "Unit in number of units delivered (keep the space character!)"
+                    )
+                    Text("(Eventual Glucose - Target) / ISF =").font(.callout).italic()
+                    let color: Color = (state.percentage != 100 && state.insulin > 0) ? .secondary : .blue
+                    let fontWeight: Font.Weight = (state.percentage != 100 && state.insulin > 0) ? .regular : .bold
+                    HStack {
+                        Text(" = ").font(.callout)
+                        Text(state.insulin.formatted() + unit).font(.callout).foregroundColor(color).fontWeight(fontWeight)
+                    }
+                    if state.percentage != 100, state.insulin > 0 {
+                        Divider()
+                        HStack { Text(state.percentage.formatted() + " % ->").font(.callout).foregroundColor(.secondary)
+                            Text(
+                                state.insulinRecommended.formatted() + unit
+                            ).font(.callout).foregroundColor(.blue).bold()
+                        }
+                    }
+                }
+                // Warning
+                VStack {
+                    Divider()
+                    if state.error, state.insulinRecommended > 0 {
+                        Text("Warning!").font(.callout).foregroundColor(.orange).bold()
+                        Text(NSLocalizedString(state.errorString, comment: "")).font(.caption)
+                        Divider()
+                    }
+                }.padding(.horizontal, 10)
+                // Footer. Warning string .
+                if !(state.error && state.insulinRecommended > 0) {
+                    VStack {
+                        Text(
+                            "Carbs and previous insulin are included in the glucose prediction, but if the Eventual Glucose is lower than the Target Glucose, a bolus will not be recommended."
+                        ).font(.caption2).foregroundColor(.secondary)
+                    }.padding(20)
+                }
+                // Hide button
+                VStack {
+                    Button { presentInfo = false }
+                    label: { Text("Hide") }.frame(maxWidth: .infinity, alignment: .center).font(.callout)
+                        .foregroundColor(.blue)
+                }.padding(.bottom, 10)
+            }
+            .background(
+                RoundedRectangle(cornerRadius: 8, style: .continuous)
+                    .fill(Color(colorScheme == .dark ? UIColor.systemGray4 : UIColor.systemGray4))
+            )
         }
         }
     }
     }
 }
 }

+ 4 - 4
FreeAPS/Sources/Modules/CGM/View/CGMSetupView.swift

@@ -30,15 +30,15 @@ extension CGM {
             }
             }
 
 
             switch CGMType {
             switch CGMType {
-            case .dexcomG6:
-                setupViewController = G6CGMManager.setupViewController(
+            case .dexcomG5:
+                setupViewController = G5CGMManager.setupViewController(
                     bluetoothProvider: bluetoothManager,
                     bluetoothProvider: bluetoothManager,
                     displayGlucoseUnitObservable: displayGlucoseUnitObservable,
                     displayGlucoseUnitObservable: displayGlucoseUnitObservable,
                     colorPalette: .default,
                     colorPalette: .default,
                     allowDebugFeatures: false
                     allowDebugFeatures: false
                 )
                 )
-            case .dexcomG5:
-                setupViewController = G5CGMManager.setupViewController(
+            case .dexcomG6:
+                setupViewController = G6CGMManager.setupViewController(
                     bluetoothProvider: bluetoothManager,
                     bluetoothProvider: bluetoothManager,
                     displayGlucoseUnitObservable: displayGlucoseUnitObservable,
                     displayGlucoseUnitObservable: displayGlucoseUnitObservable,
                     colorPalette: .default,
                     colorPalette: .default,

+ 46 - 0
FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -1,13 +1,19 @@
+import CoreData
 import SwiftUI
 import SwiftUI
 
 
 extension DataTable {
 extension DataTable {
     final class StateModel: BaseStateModel<Provider> {
     final class StateModel: BaseStateModel<Provider> {
         @Injected() var broadcaster: Broadcaster!
         @Injected() var broadcaster: Broadcaster!
         @Injected() var unlockmanager: UnlockManager!
         @Injected() var unlockmanager: UnlockManager!
+        @Injected() private var storage: FileStorage!
+
+        let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
 
 
         @Published var mode: Mode = .treatments
         @Published var mode: Mode = .treatments
         @Published var treatments: [Treatment] = []
         @Published var treatments: [Treatment] = []
         @Published var glucose: [Glucose] = []
         @Published var glucose: [Glucose] = []
+        @Published var manualGlcuose: Decimal = 0
+
         var units: GlucoseUnits = .mmolL
         var units: GlucoseUnits = .mmolL
 
 
         override func subscribe() {
         override func subscribe() {
@@ -132,6 +138,46 @@ extension DataTable {
         func deleteGlucose(at index: Int) {
         func deleteGlucose(at index: Int) {
             let id = glucose[index].id
             let id = glucose[index].id
             provider.deleteGlucose(id: id)
             provider.deleteGlucose(id: id)
+
+            let fetchRequest: NSFetchRequest<NSFetchRequestResult>
+            fetchRequest = NSFetchRequest(entityName: "Readings")
+            fetchRequest.predicate = NSPredicate(format: "id == %@", id)
+            let deleteRequest = NSBatchDeleteRequest(
+                fetchRequest: fetchRequest
+            )
+            deleteRequest.resultType = .resultTypeObjectIDs
+            do {
+                let deleteResult = try coredataContext.execute(deleteRequest) as? NSBatchDeleteResult
+                if let objectIDs = deleteResult?.result as? [NSManagedObjectID] {
+                    NSManagedObjectContext.mergeChanges(
+                        fromRemoteContextSave: [NSDeletedObjectsKey: objectIDs],
+                        into: [coredataContext]
+                    )
+                }
+            } catch {
+                // To do: handle any thrown errors.
+            }
+            // try? coredataContext.save()
+        }
+
+        func addManualGlucose() {
+            let glucose = units == .mmolL ? manualGlcuose.asMgdL : manualGlcuose
+            let now = Date()
+            let id = UUID().uuidString
+
+            let saveToJSON = BloodGlucose(
+                _id: id,
+                direction: nil,
+                date: Decimal(now.timeIntervalSince1970) * 1000,
+                dateString: now,
+                unfiltered: nil,
+                filtered: nil,
+                noise: nil,
+                glucose: Int(glucose),
+                type: "Manual"
+            )
+            provider.glucoseStorage.storeGlucose([saveToJSON])
+            debug(.default, "Manual Glucose saved to glucose.json")
         }
         }
     }
     }
 }
 }

+ 37 - 1
FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift

@@ -1,3 +1,4 @@
+import CoreData
 import SwiftUI
 import SwiftUI
 import Swinject
 import Swinject
 
 
@@ -8,9 +9,11 @@ extension DataTable {
 
 
         @State private var isRemoveCarbsAlertPresented = false
         @State private var isRemoveCarbsAlertPresented = false
         @State private var removeCarbsAlert: Alert?
         @State private var removeCarbsAlert: Alert?
-
         @State private var isRemoveInsulinAlertPresented = false
         @State private var isRemoveInsulinAlertPresented = false
         @State private var removeInsulinAlert: Alert?
         @State private var removeInsulinAlert: Alert?
+        @State private var newGlucose = false
+
+        @Environment(\.colorScheme) var colorScheme
 
 
         private var glucoseFormatter: NumberFormatter {
         private var glucoseFormatter: NumberFormatter {
             let formatter = NumberFormatter()
             let formatter = NumberFormatter()
@@ -54,6 +57,35 @@ extension DataTable {
                 leading: Button("Close", action: state.hideModal),
                 leading: Button("Close", action: state.hideModal),
                 trailing: state.mode == .glucose ? EditButton().asAny() : EmptyView().asAny()
                 trailing: state.mode == .glucose ? EditButton().asAny() : EmptyView().asAny()
             )
             )
+            .popup(isPresented: newGlucose, alignment: .top, direction: .bottom) {
+                VStack(spacing: 20) {
+                    HStack {
+                        Text("New Glucose")
+                        DecimalTextField(" ... ", value: $state.manualGlcuose, formatter: glucoseFormatter)
+                        Text(state.units.rawValue)
+                    }.padding(.horizontal, 20)
+                    HStack {
+                        let limitLow: Decimal = state.units == .mmolL ? 2.2 : 40
+                        let limitHigh: Decimal = state.units == .mmolL ? 21 : 380
+                        Button { newGlucose = false }
+                        label: { Text("Cancel") }.frame(maxWidth: .infinity, alignment: .leading)
+
+                        Button {
+                            state.addManualGlucose()
+                            newGlucose = false
+                        }
+                        label: { Text("Save") }
+                            .frame(maxWidth: .infinity, alignment: .trailing)
+                            .disabled(state.manualGlcuose < limitLow || state.manualGlcuose > limitHigh)
+
+                    }.padding(20)
+                }
+                .frame(maxHeight: 140)
+                .background(
+                    RoundedRectangle(cornerRadius: 8, style: .continuous)
+                        .fill(Color(colorScheme == .dark ? UIColor.systemGray2 : UIColor.systemGray6))
+                )
+            }
         }
         }
 
 
         private var treatmentsList: some View {
         private var treatmentsList: some View {
@@ -66,6 +98,10 @@ extension DataTable {
 
 
         private var glucoseList: some View {
         private var glucoseList: some View {
             List {
             List {
+                Button { newGlucose = true }
+                label: { Text("Add") }.frame(maxWidth: .infinity, alignment: .trailing)
+                    .padding(.trailing, 20)
+
                 ForEach(state.glucose) { item in
                 ForEach(state.glucose) { item in
                     glucoseView(item)
                     glucoseView(item)
                 }.onDelete(perform: deleteGlucose)
                 }.onDelete(perform: deleteGlucose)

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

@@ -667,8 +667,9 @@ extension MainChartView {
                 path.addLine(to: CGPoint(x: lastPoint.x, y: Config.basalHeight))
                 path.addLine(to: CGPoint(x: lastPoint.x, y: Config.basalHeight))
                 path.addLine(to: CGPoint(x: 0, y: Config.basalHeight))
                 path.addLine(to: CGPoint(x: 0, y: Config.basalHeight))
             }
             }
-
-            let endDateTime = dayAgoTime + min(max(screenHours, 2), 24).hours.timeInterval + min(max(screenHours, 2), 24).hours
+            let adjustForOptionalExtraHours = screenHours > 12 ? screenHours - 12 : 0
+            let endDateTime = dayAgoTime + min(max(screenHours - adjustForOptionalExtraHours, 12), 24).hours
+                .timeInterval + min(max(screenHours - adjustForOptionalExtraHours, 12), 24).hours
                 .timeInterval
                 .timeInterval
             let autotunedBasalPoints = findRegularBasalPoints(
             let autotunedBasalPoints = findRegularBasalPoints(
                 timeBegin: dayAgoTime,
                 timeBegin: dayAgoTime,

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

@@ -502,7 +502,7 @@ extension Home {
                             .padding(8)
                             .padding(8)
                     }.foregroundColor(.loopGreen)
                     }.foregroundColor(.loopGreen)
                     Spacer()
                     Spacer()
-                    Button { state.showModal(for: .bolus(waitForSuggestion: false)) }
+                    Button { state.showModal(for: .bolus(waitForSuggestion: false, manualBolus: false)) }
                     label: {
                     label: {
                         Image("bolus")
                         Image("bolus")
                             .renderingMode(.template)
                             .renderingMode(.template)

+ 125 - 38
FreeAPS/Sources/Modules/Stat/View/ChartsView.swift

@@ -1,9 +1,3 @@
-//
-//  FilteredLoopsView.swift
-//  FreeAPS
-//
-//  Created by Jon Mårtensson on 2023-05-29.
-//
 import Charts
 import Charts
 import CoreData
 import CoreData
 import SwiftDate
 import SwiftDate
@@ -25,7 +19,18 @@ struct ChartsView: View {
     var body: some View {
     var body: some View {
         glucoseChart
         glucoseChart
         Rectangle().fill(.cyan.opacity(0.2)).frame(maxHeight: 3)
         Rectangle().fill(.cyan.opacity(0.2)).frame(maxHeight: 3)
-        if standing { tirChart } else { standingTIRchart }
+        if standing {
+            VStack {
+                tirChart
+                Rectangle().fill(.cyan.opacity(0.2)).frame(maxHeight: 3)
+                groupedGlucoseStatsLaying
+            }
+        } else {
+            HStack(spacing: 20) {
+                standingTIRchart
+                groupedGlucose
+            }
+        }
     }
     }
 
 
     init(
     init(
@@ -96,12 +101,11 @@ struct ChartsView: View {
                     units == .mmolL ? 15 : 270
                     units == .mmolL ? 15 : 270
                 ]
                 ]
             )
             )
-        } // .background(.gray.opacity(0.05))
+        }
     }
     }
 
 
     var tirChart: some View {
     var tirChart: some View {
         let fetched = tir()
         let fetched = tir()
-
         let low = lowLimit * (units == .mmolL ? Decimal(conversionFactor) : 1)
         let low = lowLimit * (units == .mmolL ? Decimal(conversionFactor) : 1)
         let high = highLimit * (units == .mmolL ? Decimal(conversionFactor) : 1)
         let high = highLimit * (units == .mmolL ? Decimal(conversionFactor) : 1)
 
 
@@ -110,7 +114,7 @@ struct ChartsView: View {
                 type: NSLocalizedString(
                 type: NSLocalizedString(
                     "Low",
                     "Low",
                     comment: ""
                     comment: ""
-                ) + " (\(low.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))))",
+                ) + " (\(low.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))))",
                 percent: fetched[0].decimal
                 percent: fetched[0].decimal
             ),
             ),
             .init(type: NSLocalizedString("In Range", comment: ""), percent: fetched[1].decimal),
             .init(type: NSLocalizedString("In Range", comment: ""), percent: fetched[1].decimal),
@@ -118,7 +122,7 @@ struct ChartsView: View {
                 type: NSLocalizedString(
                 type: NSLocalizedString(
                     "High",
                     "High",
                     comment: ""
                     comment: ""
-                ) + " (\(high.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))))",
+                ) + " (\(high.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))))",
                 percent: fetched[2].decimal
                 percent: fetched[2].decimal
             )
             )
         ]
         ]
@@ -138,12 +142,12 @@ struct ChartsView: View {
             NSLocalizedString(
             NSLocalizedString(
                 "Low",
                 "Low",
                 comment: ""
                 comment: ""
-            ) + " (\(low.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))))": .red,
+            ) + " (\(low.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))))": .red,
             NSLocalizedString("In Range", comment: ""): .green,
             NSLocalizedString("In Range", comment: ""): .green,
             NSLocalizedString(
             NSLocalizedString(
                 "High",
                 "High",
                 comment: ""
                 comment: ""
-            ) + " (\(high.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))))": .orange
+            ) + " (\(high.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))))": .orange
         ]).frame(maxHeight: 25)
         ]).frame(maxHeight: 25)
     }
     }
 
 
@@ -151,48 +155,131 @@ struct ChartsView: View {
         let fetched = tir()
         let fetched = tir()
         let low = lowLimit * (units == .mmolL ? Decimal(conversionFactor) : 1)
         let low = lowLimit * (units == .mmolL ? Decimal(conversionFactor) : 1)
         let high = highLimit * (units == .mmolL ? Decimal(conversionFactor) : 1)
         let high = highLimit * (units == .mmolL ? Decimal(conversionFactor) : 1)
+        let fraction = units == .mmolL ? 1 : 0
         let data: [ShapeModel] = [
         let data: [ShapeModel] = [
             .init(
             .init(
                 type: NSLocalizedString(
                 type: NSLocalizedString(
                     "Low",
                     "Low",
                     comment: ""
                     comment: ""
-                ) + " (\(low.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))))",
+                ) + " (\(low.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))))",
                 percent: fetched[0].decimal
                 percent: fetched[0].decimal
             ),
             ),
-            .init(type: NSLocalizedString("In Range", comment: ""), percent: fetched[1].decimal),
+            .init(
+                type: "> \(low.formatted(.number.precision(.fractionLength(fraction)))) - < \(high.formatted(.number.precision(.fractionLength(fraction))))",
+                percent: fetched[1].decimal
+            ),
             .init(
             .init(
                 type: NSLocalizedString(
                 type: NSLocalizedString(
                     "High",
                     "High",
                     comment: ""
                     comment: ""
-                ) + " (\(high.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))))",
+                ) + " (\(high.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))))",
                 percent: fetched[2].decimal
                 percent: fetched[2].decimal
             )
             )
         ]
         ]
+        return Chart(data) { shape in
+            BarMark(
+                x: .value("Shape", shape.type),
+                y: .value("Percentage", shape.percent)
+            )
+            .foregroundStyle(by: .value("Group", shape.type))
+            .annotation(position: shape.percent > 19 ? .overlay : .automatic, alignment: .center) {
+                Text(shape.percent == 0 ? "" : "\(shape.percent, format: .number.precision(.fractionLength(0)))")
+            }
+        }
+        .chartXAxis(.hidden)
+        .chartYAxis {
+            AxisMarks(
+                format: Decimal.FormatStyle.Percent.percent.scale(1)
+            )
+        }
+        .chartForegroundStyleScale([
+            NSLocalizedString(
+                "Low",
+                comment: ""
+            ) + " (≤ \(low.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))))": .red,
+            "> \(low.formatted(.number.precision(.fractionLength(fraction)))) - < \(high.formatted(.number.precision(.fractionLength(fraction))))": .green,
+            NSLocalizedString(
+                "High",
+                comment: ""
+            ) + " (≥ \(high.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))))": .orange
+        ])
+    }
 
 
-        return VStack(alignment: .center) {
-            Chart(data) { shape in
-                BarMark(
-                    x: .value("Shape", shape.type),
-                    y: .value("Percentage", shape.percent)
-                )
-                .foregroundStyle(by: .value("Group", shape.type))
-                .annotation(position: shape.percent <= 9 ? .top : .overlay, alignment: .center) {
-                    Text(shape.percent == 0 ? "" : "\(shape.percent, format: .number.precision(.fractionLength(0))) %")
+    var groupedGlucose: some View {
+        VStack(alignment: .leading, spacing: 20) {
+            let glucose = fetchRequest
+            let mapGlucose = glucose.compactMap({ each in each.glucose })
+            if !mapGlucose.isEmpty {
+                let mapGlucoseAcuteLow = mapGlucose.filter({ $0 < Int16(3.3 / 0.0555) })
+                let mapGlucoseHigh = mapGlucose.filter({ $0 > Int16(11 / 0.0555) })
+                let mapGlucoseNormal = mapGlucose.filter({ $0 > Int16(3.8 / 0.0555) && $0 < Int16(7.9 / 0.0555) })
+
+                HStack {
+                    let value = Double(mapGlucoseHigh.count * 100 / mapGlucose.count)
+                    if value != 0 {
+                        Text(units == .mmolL ? ">  11  " : ">  200 ").foregroundColor(.secondary)
+                        Text(value.formatted()).foregroundColor(.orange)
+                        Text("%").foregroundColor(.secondary)
+                    }
+                }.font(.caption)
+
+                HStack {
+                    let value = Double(mapGlucoseNormal.count * 100 / mapGlucose.count)
+                    if value != 0 {
+                        Text(units == .mmolL ? "3.9-7.8" : "70-140").foregroundColor(.secondary)
+                        Text(value.formatted()).foregroundColor(.green)
+                        Text("%").foregroundColor(.secondary)
+                    }
+                }.font(.caption)
+
+                HStack {
+                    let value = Double(mapGlucoseAcuteLow.count * 100 / mapGlucose.count)
+                    if value != 0 {
+                        Text(units == .mmolL ? "<  3.3 " : "<  59  ").foregroundColor(.secondary)
+                        Text(value.formatted()).foregroundColor(.red)
+                        Text("%").foregroundColor(.secondary)
+                    }
+                }.font(.caption)
+            }
+        }
+    }
+
+    var groupedGlucoseStatsLaying: some View {
+        HStack {
+            let glucose = fetchRequest
+            let mapGlucose = glucose.compactMap({ each in each.glucose })
+            if !mapGlucose.isEmpty {
+                let mapGlucoseLow = mapGlucose.filter({ $0 < Int16(3.3 / 0.0555) })
+                let mapGlucoseNormal = mapGlucose.filter({ $0 > Int16(3.8 / 0.0555) && $0 < Int16(7.9 / 0.0555) })
+                let mapGlucoseAcuteHigh = mapGlucose.filter({ $0 > Int16(11 / 0.0555) })
+
+                HStack {
+                    let value = Double(mapGlucoseLow.count * 100 / mapGlucose.count)
+                    if value != 0 {
+                        Text(units == .mmolL ? "< 3.3" : "< 59").font(.caption2).foregroundColor(.secondary)
+                        Text(value.formatted()).font(.caption).foregroundColor(value == 0 ? .green : .red)
+                        Text("%").font(.caption)
+                    }
+                }
+                Spacer()
+                HStack {
+                    let value = Double(mapGlucoseNormal.count * 100 / mapGlucose.count)
+                    if value != 0 {
+                        Text(units == .mmolL ? "3.9-7.8" : "70-140").foregroundColor(.secondary)
+                        Text(value.formatted()).foregroundColor(.green)
+                        Text("%").foregroundColor(.secondary)
+                    }
+                }.font(.caption)
+                Spacer()
+                HStack {
+                    let value = Double(mapGlucoseAcuteHigh.count * 100 / mapGlucose.count)
+                    if value != 0 {
+                        Text(units == .mmolL ? "> 11.0" : "> 216").font(.caption).foregroundColor(.secondary)
+                        Text(value.formatted()).font(.caption).foregroundColor(value == 0 ? .green : .orange)
+                        Text("%").font(.caption)
+                    }
                 }
                 }
             }
             }
-            .chartYAxis(.hidden)
-            .chartLegend(.hidden)
-            .chartForegroundStyleScale([
-                NSLocalizedString(
-                    "Low",
-                    comment: ""
-                ) + " (\(low.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))))": .red,
-                NSLocalizedString("In Range", comment: ""): .green,
-                NSLocalizedString(
-                    "High",
-                    comment: ""
-                ) + " (\(high.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))))": .orange
-            ])
         }
         }
     }
     }
 
 

+ 23 - 33
FreeAPS/Sources/Modules/Stat/View/StatsView.swift

@@ -1,9 +1,3 @@
-//
-//  FilteredLoopsView.swift
-//  FreeAPS
-//
-//  Created by Jon Mårtensson on 2023-05-29.
-//
 import CoreData
 import CoreData
 import SwiftDate
 import SwiftDate
 import SwiftUI
 import SwiftUI
@@ -55,33 +49,29 @@ struct StatsView: View {
     }
     }
 
 
     var loops: some View {
     var loops: some View {
-        VStack(spacing: 10) {
-            let loops = fetchRequest
-
-            // First date
-            let previous = loops.last?.end ?? Date()
-            // Last date (recent)
-            let current = loops.first?.start ?? Date()
-
-            // Total time in days
-            let totalTime = (current - previous).timeInterval / 8.64E4
-
-            let durationArray = loops.compactMap({ each in each.duration })
-            let durationArrayCount = durationArray.count
-            // var durationAverage = durationArray.reduce(0, +) / Double(durationArrayCount)
-            let medianDuration = medianCalculationDouble(array: durationArray)
-            let successsNR = loops.compactMap({ each in each.loopStatus }).filter({ each in each!.contains("Success") })
-                .count
-            let errorNR = durationArrayCount - successsNR
-            let successRate: Double? = (Double(successsNR) / Double(successsNR + errorNR)) * 100
-
-            let loopNr = totalTime <= 1 ? Double(successsNR + errorNR) : round(Double(successsNR + errorNR) / totalTime)
-
-            let intervalArray = loops.compactMap({ each in each.interval as Double })
-            let intervalAverage = intervalArray.reduce(0, +) / Double(intervalArray.count)
-            // let maximumInterval = intervalArray.max()
-            // let minimumInterval = intervalArray.min()
-
+        let loops = fetchRequest
+        // First date
+        let previous = loops.last?.end ?? Date()
+        // Last date (recent)
+        let current = loops.first?.start ?? Date()
+        // Total time in days
+        let totalTime = (current - previous).timeInterval / 8.64E4
+
+        let durationArray = loops.compactMap({ each in each.duration })
+        let durationArrayCount = durationArray.count
+        // var durationAverage = durationArray.reduce(0, +) / Double(durationArrayCount)
+        let medianDuration = medianCalculationDouble(array: durationArray)
+        let successsNR = loops.compactMap({ each in each.loopStatus }).filter({ each in each!.contains("Success") }).count
+        let errorNR = durationArrayCount - successsNR
+        let total = Double(successsNR + errorNR) == 0 ? 1 : Double(successsNR + errorNR)
+        let successRate: Double? = (Double(successsNR) / total) * 100
+        let loopNr = totalTime <= 1 ? total : round(total / (totalTime != 0 ? totalTime : 1))
+        let intervalArray = loops.compactMap({ each in each.interval as Double })
+        let count = intervalArray.count != 0 ? intervalArray.count : 1
+        let intervalAverage = intervalArray.reduce(0, +) / Double(count)
+        // let maximumInterval = intervalArray.max()
+        // let minimumInterval = intervalArray.min()
+        return VStack(spacing: 10) {
             HStack(spacing: 35) {
             HStack(spacing: 35) {
                 VStack(spacing: 5) {
                 VStack(spacing: 5) {
                     Text("Loops").font(.subheadline).foregroundColor(headline)
                     Text("Loops").font(.subheadline).foregroundColor(headline)

+ 2 - 2
FreeAPS/Sources/Modules/StatConfig/StatConfigStateModel.swift

@@ -26,7 +26,7 @@ extension StatConfig {
             subscribeSetting(\.oneDimensionalGraph, on: $oneDimensionalGraph) { oneDimensionalGraph = $0 }
             subscribeSetting(\.oneDimensionalGraph, on: $oneDimensionalGraph) { oneDimensionalGraph = $0 }
 
 
             subscribeSetting(\.low, on: $low, initial: {
             subscribeSetting(\.low, on: $low, initial: {
-                let value = max(min($0, 120), 40)
+                let value = max(min($0, 90), 40)
                 low = units == .mmolL ? value.asMmolL : value
                 low = units == .mmolL ? value.asMmolL : value
             }, map: {
             }, map: {
                 guard units == .mmolL else { return $0 }
                 guard units == .mmolL else { return $0 }
@@ -34,7 +34,7 @@ extension StatConfig {
             })
             })
 
 
             subscribeSetting(\.high, on: $high, initial: {
             subscribeSetting(\.high, on: $high, initial: {
-                let value = max(min($0, 270), 130)
+                let value = max(min($0, 270), 110)
                 high = units == .mmolL ? value.asMmolL : value
                 high = units == .mmolL ? value.asMmolL : value
             }, map: {
             }, map: {
                 guard units == .mmolL else { return $0 }
                 guard units == .mmolL else { return $0 }

+ 2 - 2
FreeAPS/Sources/Modules/TargetsEditor/TargetsEditorDataFlow.swift

@@ -9,9 +9,9 @@ enum TargetsEditor {
         var highIndex = 0
         var highIndex = 0
         var timeIndex = 0
         var timeIndex = 0
 
 
-        init(lowIndex: Int, highIndex: Int, timeIndex: Int) {
+        init(lowIndex: Int, highIndex _: Int, timeIndex: Int) {
             self.lowIndex = lowIndex
             self.lowIndex = lowIndex
-            self.highIndex = highIndex
+            highIndex = lowIndex
             self.timeIndex = timeIndex
             self.timeIndex = timeIndex
         }
         }
 
 

+ 3 - 3
FreeAPS/Sources/Modules/TargetsEditor/TargetsEditorStateModel.swift

@@ -28,7 +28,7 @@ extension TargetsEditor {
             items = profile.targets.map { value in
             items = profile.targets.map { value in
                 let timeIndex = timeValues.firstIndex(of: Double(value.offset * 60)) ?? 0
                 let timeIndex = timeValues.firstIndex(of: Double(value.offset * 60)) ?? 0
                 let lowIndex = rateValues.firstIndex(of: Double(value.low)) ?? 0
                 let lowIndex = rateValues.firstIndex(of: Double(value.low)) ?? 0
-                let highIndex = rateValues.firstIndex(of: Double(value.high)) ?? 0
+                let highIndex = lowIndex
                 return Item(lowIndex: lowIndex, highIndex: highIndex, timeIndex: timeIndex)
                 return Item(lowIndex: lowIndex, highIndex: highIndex, timeIndex: timeIndex)
             }
             }
         }
         }
@@ -40,7 +40,7 @@ extension TargetsEditor {
             if let last = items.last {
             if let last = items.last {
                 time = last.timeIndex + 1
                 time = last.timeIndex + 1
                 low = last.lowIndex
                 low = last.lowIndex
-                high = last.highIndex
+                high = low
             }
             }
 
 
             let newItem = Item(lowIndex: low, highIndex: high, timeIndex: time)
             let newItem = Item(lowIndex: low, highIndex: high, timeIndex: time)
@@ -56,7 +56,7 @@ extension TargetsEditor {
                 let date = Date(timeIntervalSince1970: self.timeValues[item.timeIndex])
                 let date = Date(timeIntervalSince1970: self.timeValues[item.timeIndex])
                 let minutes = Int(date.timeIntervalSince1970 / 60)
                 let minutes = Int(date.timeIntervalSince1970 / 60)
                 let low = Decimal(self.rateValues[item.lowIndex])
                 let low = Decimal(self.rateValues[item.lowIndex])
-                let high = Decimal(self.rateValues[item.highIndex])
+                let high = low
                 return BGTargetEntry(low: low, high: high, start: fotmatter.string(from: date), offset: minutes)
                 return BGTargetEntry(low: low, high: high, start: fotmatter.string(from: date), offset: minutes)
             }
             }
             let profile = BGTargets(units: units, userPrefferedUnits: settingsManager.settings.units, targets: targets)
             let profile = BGTargets(units: units, userPrefferedUnits: settingsManager.settings.units, targets: targets)

+ 1 - 17
FreeAPS/Sources/Modules/TargetsEditor/View/TargetsEditorRootView.swift

@@ -54,8 +54,7 @@ extension TargetsEditor {
             GeometryReader { geometry in
             GeometryReader { geometry in
                 VStack {
                 VStack {
                     HStack {
                     HStack {
-                        Text("Low target").frame(width: geometry.size.width / 3)
-                        Text("High target").frame(width: geometry.size.width / 3)
+                        Text("Target").frame(width: geometry.size.width / 3)
                         Text("Time").frame(width: geometry.size.width / 3)
                         Text("Time").frame(width: geometry.size.width / 3)
                     }
                     }
                     HStack(spacing: 0) {
                     HStack(spacing: 0) {
@@ -69,17 +68,6 @@ extension TargetsEditor {
                         }
                         }
                         .frame(maxWidth: geometry.size.width / 3)
                         .frame(maxWidth: geometry.size.width / 3)
                         .clipped()
                         .clipped()
-                        Picker(selection: $state.items[index].highIndex, label: EmptyView()) {
-                            ForEach(0 ..< state.rateValues.count, id: \.self) { i in
-                                Text(
-                                    self.rateFormatter
-                                        .string(from: state.rateValues[i] as NSNumber) ?? ""
-                                ).tag(i)
-                            }
-                        }
-                        .frame(maxWidth: geometry.size.width / 3)
-                        .clipped()
-
                         Picker(selection: $state.items[index].timeIndex, label: EmptyView()) {
                         Picker(selection: $state.items[index].timeIndex, label: EmptyView()) {
                             ForEach(0 ..< state.timeValues.count, id: \.self) { i in
                             ForEach(0 ..< state.timeValues.count, id: \.self) { i in
                                 Text(
                                 Text(
@@ -106,10 +94,6 @@ extension TargetsEditor {
                             Text(
                             Text(
                                 "\(rateFormatter.string(from: state.rateValues[item.lowIndex] as NSNumber) ?? "0")"
                                 "\(rateFormatter.string(from: state.rateValues[item.lowIndex] as NSNumber) ?? "0")"
                             )
                             )
-                            Text("–").foregroundColor(.secondary)
-                            Text(
-                                "\(rateFormatter.string(from: state.rateValues[item.highIndex] as NSNumber) ?? "0")"
-                            )
                             Text("\(state.units.rawValue)").foregroundColor(.secondary)
                             Text("\(state.units.rawValue)").foregroundColor(.secondary)
                             Spacer()
                             Spacer()
                             Text("starts at").foregroundColor(.secondary)
                             Text("starts at").foregroundColor(.secondary)

+ 3 - 3
FreeAPS/Sources/Router/Screen.swift

@@ -16,7 +16,7 @@ enum Screen: Identifiable, Hashable {
     case preferencesEditor
     case preferencesEditor
     case addCarbs
     case addCarbs
     case addTempTarget
     case addTempTarget
-    case bolus(waitForSuggestion: Bool)
+    case bolus(waitForSuggestion: Bool, manualBolus: Bool)
     case manualTempBasal
     case manualTempBasal
     case autotuneConfig
     case autotuneConfig
     case dataTable
     case dataTable
@@ -67,8 +67,8 @@ extension Screen {
             AddCarbs.RootView(resolver: resolver)
             AddCarbs.RootView(resolver: resolver)
         case .addTempTarget:
         case .addTempTarget:
             AddTempTarget.RootView(resolver: resolver)
             AddTempTarget.RootView(resolver: resolver)
-        case let .bolus(waitForSuggestion):
-            Bolus.RootView(resolver: resolver, waitForSuggestion: waitForSuggestion)
+        case let .bolus(waitForSuggestion, manualBolus):
+            Bolus.RootView(resolver: resolver, waitForSuggestion: waitForSuggestion, manualBolus: manualBolus)
         case .manualTempBasal:
         case .manualTempBasal:
             ManualTempBasal.RootView(resolver: resolver)
             ManualTempBasal.RootView(resolver: resolver)
         case .autotuneConfig:
         case .autotuneConfig:

+ 8 - 2
FreeAPS/Sources/Services/WatchManager/WatchManager.swift

@@ -75,9 +75,15 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
             self.state.maxBolus = self.settingsManager.pumpSettings.maxBolus
             self.state.maxBolus = self.settingsManager.pumpSettings.maxBolus
             self.state.carbsRequired = self.suggestion?.carbsReq
             self.state.carbsRequired = self.suggestion?.carbsReq
 
 
-            let insulinRequired = self.suggestion?.insulinReq ?? 0
+            var insulinRequired = self.suggestion?.insulinReq ?? 0
+            var double: Decimal = 2
+            if (self.suggestion?.cob ?? 0) > 0 {
+                insulinRequired = self.suggestion?.insulinForManualBolus ?? 0
+                double = 1
+            }
+
             self.state.bolusRecommended = self.apsManager
             self.state.bolusRecommended = self.apsManager
-                .roundBolus(amount: max(insulinRequired * (self.settingsManager.settings.insulinReqPercentage / 100) * 2, 0))
+                .roundBolus(amount: max(insulinRequired * (self.settingsManager.settings.insulinReqPercentage / 100) * double, 0))
 
 
             self.state.iob = self.suggestion?.iob
             self.state.iob = self.suggestion?.iob
             self.state.cob = self.suggestion?.cob
             self.state.cob = self.suggestion?.cob

+ 3 - 8
FreeAPSWatch WatchKit Extension/Views/MainView.swift

@@ -176,8 +176,8 @@ struct MainView: View {
                         Image(systemName: "arrow.up.arrow.down")
                         Image(systemName: "arrow.up.arrow.down")
                             .renderingMode(.template)
                             .renderingMode(.template)
                             .resizable()
                             .resizable()
-                            .frame(width: 16, height: 16)
-                            .foregroundColor(.blue)
+                            .frame(width: 12, height: 12)
+                            .foregroundColor(.loopGreen)
                         Text("\(isf)")
                         Text("\(isf)")
                             .fontWeight(.regular)
                             .fontWeight(.regular)
                             .font(.caption2)
                             .font(.caption2)
@@ -189,12 +189,7 @@ struct MainView: View {
                     Spacer()
                     Spacer()
                     let override: String = state.override != nil ? state.override! : "-"
                     let override: String = state.override != nil ? state.override! : "-"
                     HStack {
                     HStack {
-                        Image(systemName: "person.3.sequence.fill")
-                            .renderingMode(.template)
-                            .resizable()
-                            .frame(width: 24, height: 12)
-                            .foregroundColor(.blue)
-                        Text("\(override)")
+                        Text("👤 \(override)")
                             .fontWeight(.regular)
                             .fontWeight(.regular)
                             .font(.caption2)
                             .font(.caption2)
                             .scaledToFill()
                             .scaledToFill()