Explorar el Código

Merge branch 'dev'

Conflicts:
	FreeAPS/Sources/Localizations/Main/uk.lproj/Localizable.strings
Jon Mårtensson hace 2 años
padre
commit
44fc78d4fa
Se han modificado 61 ficheros con 739 adiciones y 622 borrados
  1. 4 0
      FreeAPS.xcodeproj/project.pbxproj
  2. 34 0
      FreeAPS/Resources/Assets.xcassets/Colors/darkGray.colorset/Contents.json
  3. 1 1
      FreeAPS/Resources/javascript/bundle/autotune-core.js
  4. 1 1
      FreeAPS/Resources/javascript/bundle/determine-basal.js
  5. 1 1
      FreeAPS/Resources/javascript/bundle/profile.js
  6. 2 2
      FreeAPS/Resources/javascript/prepare/autotune-prep.js
  7. 1 1
      FreeAPS/Resources/javascript/prepare/meal.js
  8. 251 361
      FreeAPS/Sources/APS/APSManager.swift
  9. 85 11
      FreeAPS/Sources/APS/Storage/CarbsStorage.swift
  10. 2 0
      FreeAPS/Sources/APS/Storage/GlucoseStorage.swift
  11. 12 0
      FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift
  12. 1 0
      FreeAPS/Sources/Helpers/Color+Extensions.swift
  13. 1 4
      FreeAPS/Sources/Localizations/Main/ar.lproj/Localizable.strings
  14. 1 3
      FreeAPS/Sources/Localizations/Main/ca.lproj/Localizable.strings
  15. 1 4
      FreeAPS/Sources/Localizations/Main/da.lproj/Localizable.strings
  16. 1 4
      FreeAPS/Sources/Localizations/Main/de.lproj/Localizable.strings
  17. 1 4
      FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings
  18. 1 4
      FreeAPS/Sources/Localizations/Main/es.lproj/Localizable.strings
  19. 1 4
      FreeAPS/Sources/Localizations/Main/fi.lproj/Localizable.strings
  20. 1 4
      FreeAPS/Sources/Localizations/Main/fr.lproj/Localizable.strings
  21. 1 4
      FreeAPS/Sources/Localizations/Main/he.lproj/Localizable.strings
  22. 1 1
      FreeAPS/Sources/Localizations/Main/it.lproj/Localizable.strings
  23. 1 1
      FreeAPS/Sources/Localizations/Main/nb.lproj/Localizable.strings
  24. 1 4
      FreeAPS/Sources/Localizations/Main/nl.lproj/Localizable.strings
  25. 1 4
      FreeAPS/Sources/Localizations/Main/pl.lproj/Localizable.strings
  26. 1 4
      FreeAPS/Sources/Localizations/Main/pt-BR.lproj/Localizable.strings
  27. 1 4
      FreeAPS/Sources/Localizations/Main/pt-PT.lproj/Localizable.strings
  28. 1 4
      FreeAPS/Sources/Localizations/Main/ru.lproj/Localizable.strings
  29. 1 4
      FreeAPS/Sources/Localizations/Main/sk.lproj/Localizable.strings
  30. 1 4
      FreeAPS/Sources/Localizations/Main/sv.lproj/Localizable.strings
  31. 1 4
      FreeAPS/Sources/Localizations/Main/tr.lproj/Localizable.strings
  32. 1 7
      FreeAPS/Sources/Localizations/Main/uk.lproj/Localizable.strings
  33. 1 4
      FreeAPS/Sources/Localizations/Main/zh-Hans.lproj/Localizable.strings
  34. 4 0
      FreeAPS/Sources/Models/CarbsEntry.swift
  35. 5 0
      FreeAPS/Sources/Models/FreeAPSSettings.swift
  36. 15 0
      FreeAPS/Sources/Models/Loops.swift
  37. 4 0
      FreeAPS/Sources/Models/NightscoutTreatment.swift
  38. 9 0
      FreeAPS/Sources/Models/Statistics.swift
  39. 2 0
      FreeAPS/Sources/Models/Suggestion.swift
  40. 11 73
      FreeAPS/Sources/Modules/AddCarbs/AddCarbsStateModel.swift
  41. 1 2
      FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift
  42. 1 1
      FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift
  43. 0 13
      FreeAPS/Sources/Modules/CGM/CGMStateModel.swift
  44. 0 4
      FreeAPS/Sources/Modules/CGM/View/CGMRootView.swift
  45. 1 1
      FreeAPS/Sources/Modules/CREditor/CREditorStateModel.swift
  46. 18 2
      FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift
  47. 7 1
      FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutConfigRootView.swift
  48. 5 4
      FreeAPS/Sources/Modules/OverrideProfilesConfig/OverrideProfilesStateModel.swift
  49. 1 1
      FreeAPS/Sources/Modules/PreferencesEditor/PreferencesEditorStateModel.swift
  50. 1 1
      FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift
  51. 0 2
      FreeAPS/Sources/Modules/StatConfig/StatConfigStateModel.swift
  52. 0 1
      FreeAPS/Sources/Modules/StatConfig/View/StatConfigRootView.swift
  53. 1 1
      FreeAPS/Sources/Modules/TargetsEditor/View/TargetsEditorRootView.swift
  54. 3 0
      FreeAPS/Sources/Modules/WatchConfig/View/WatchConfigRootView.swift
  55. 2 0
      FreeAPS/Sources/Modules/WatchConfig/WatchConfigStateModel.swift
  56. 2 0
      FreeAPS/Sources/Services/Network/NightscoutManager.swift
  57. 16 8
      FreeAPS/Sources/Services/WatchManager/WatchManager.swift
  58. 1 0
      FreeAPSWatch WatchKit Extension/DataFlow.swift
  59. 208 45
      FreeAPSWatch WatchKit Extension/Views/CarbsView.swift
  60. 4 3
      FreeAPSWatch WatchKit Extension/WatchStateModel.swift
  61. 1 1
      README.md

+ 4 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -20,6 +20,7 @@
 		190EBCCB29FF13CB00BA767D /* StatConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 190EBCCA29FF13CB00BA767D /* StatConfigRootView.swift */; };
 		1927C8E62744606D00347C69 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1927C8E82744606D00347C69 /* InfoPlist.strings */; };
 		1935364028496F7D001E0B16 /* Oref2_variables.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1935363F28496F7D001E0B16 /* Oref2_variables.swift */; };
+		193F6CDD2A512C8F001240FD /* Loops.swift in Sources */ = {isa = PBXBuildFile; fileRef = 193F6CDC2A512C8F001240FD /* Loops.swift */; };
 		1967DFBE29D052C200759F30 /* Icons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1967DFBD29D052C200759F30 /* Icons.swift */; };
 		1967DFC029D053AC00759F30 /* IconSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1967DFBF29D053AC00759F30 /* IconSelection.swift */; };
 		1967DFC229D053D300759F30 /* IconImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1967DFC129D053D300759F30 /* IconImage.swift */; };
@@ -511,6 +512,7 @@
 		1927C8FB2744612600347C69 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/InfoPlist.strings; sourceTree = "<group>"; };
 		1927C8FE274489BA00347C69 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/InfoPlist.strings; sourceTree = "<group>"; };
 		1935363F28496F7D001E0B16 /* Oref2_variables.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Oref2_variables.swift; sourceTree = "<group>"; };
+		193F6CDC2A512C8F001240FD /* Loops.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Loops.swift; sourceTree = "<group>"; };
 		1967DFBD29D052C200759F30 /* Icons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Icons.swift; sourceTree = "<group>"; };
 		1967DFBF29D053AC00759F30 /* IconSelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconSelection.swift; sourceTree = "<group>"; };
 		1967DFC129D053D300759F30 /* IconImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconImage.swift; sourceTree = "<group>"; };
@@ -1600,6 +1602,7 @@
 				1967DFBD29D052C200759F30 /* Icons.swift */,
 				19D4E4EA29FC6A9F00351451 /* TIRforChart.swift */,
 				19A910352A24D6D700C8951B /* DateFilter.swift */,
+				193F6CDC2A512C8F001240FD /* Loops.swift */,
 			);
 			path = Models;
 			sourceTree = "<group>";
@@ -2631,6 +2634,7 @@
 				38F37828261260DC009DB701 /* Color+Extensions.swift in Sources */,
 				3811DE3F25C9D4A100A708ED /* SettingsStateModel.swift in Sources */,
 				CE7CA3582A064E2F004BE681 /* ListStateView.swift in Sources */,
+				193F6CDD2A512C8F001240FD /* Loops.swift in Sources */,
 				38B4F3CB25E502E200E76A18 /* WeakObjectSet.swift in Sources */,
 				38E989DD25F5021400C0CED0 /* PumpStatus.swift in Sources */,
 				38E98A2525F52C9300C0CED0 /* IssueReporter.swift in Sources */,

+ 34 - 0
FreeAPS/Resources/Assets.xcassets/Colors/darkGray.colorset/Contents.json

@@ -0,0 +1,34 @@
+{
+  "colors" : [
+    {
+      "color" : {
+        "color-space" : "extended-gray",
+        "components" : {
+          "alpha" : "1.000",
+          "white" : "0.145"
+        }
+      },
+      "idiom" : "universal"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        }
+      ],
+      "color" : {
+        "color-space" : "extended-gray",
+        "components" : {
+          "alpha" : "1.000",
+          "white" : "0.145"
+        }
+      },
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 1
FreeAPS/Resources/javascript/bundle/autotune-core.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 1
FreeAPS/Resources/javascript/bundle/determine-basal.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 1
FreeAPS/Resources/javascript/bundle/profile.js


+ 2 - 2
FreeAPS/Resources/javascript/prepare/autotune-prep.js

@@ -1,6 +1,6 @@
 function generate(pumphistory_data, profile_data, glucose_data, pumpprofile_data, carb_data = {} , categorize_uam_as_basal = false, tune_insulin_curve = false) {
-    if (typeof(profile_data.carb_ratio) === 'undefined' || profile_data.carb_ratio < 2) {
-        if (typeof(pumpprofile_data.carb_ratio) === 'undefined' || pumpprofile_data.carb_ratio < 2) {
+    if (typeof(profile_data.carb_ratio) === 'undefined' || profile_data.carb_ratio < 1) {
+        if (typeof(pumpprofile_data.carb_ratio) === 'undefined' || pumpprofile_data.carb_ratio < 1) {
             console.log('{ "carbs": 0, "mealCOB": 0, "reason": "carb_ratios ' + profile_data.carb_ratio + ' and ' + pumpprofile_data.carb_ratio + ' out of bounds" }');
             return console.error("Error: carb_ratios " + profile_data.carb_ratio + ' and ' + pumpprofile_data.carb_ratio + " out of bounds");
         } else {

+ 1 - 1
FreeAPS/Resources/javascript/prepare/meal.js

@@ -1,7 +1,7 @@
 //для monitor/meal.json параметры: monitor/pumphistory-24h-zoned.json settings/profile.json monitor/clock-zoned.json monitor/glucose.json settings/basal_profile.json monitor/carbhistory.json
 
 function generate(pumphistory_data, profile_data, clock_data, glucose_data, basalprofile_data, carbhistory = false) {
-    if (typeof(profile_data.carb_ratio) === 'undefined' || profile_data.carb_ratio < 3) {
+    if (typeof(profile_data.carb_ratio) === 'undefined' || profile_data.carb_ratio < 1) {
         return {"error":"Error: carb_ratio " + profile_data.carb_ratio + " out of bounds"};
     }
 

+ 251 - 361
FreeAPS/Sources/APS/APSManager.swift

@@ -732,7 +732,7 @@ final class BaseAPSManager: APSManager, Injectable {
         return rounded
     }
 
-    private func medianCalculation(array: [Double]) -> Double {
+    private func medianCalculationDouble(array: [Double]) -> Double {
         guard !array.isEmpty else {
             return 0
         }
@@ -745,7 +745,138 @@ final class BaseAPSManager: APSManager, Injectable {
         return sorted[length / 2]
     }
 
-    // Add to statistics.JSON
+    private func medianCalculation(array: [Int]) -> Double {
+        guard !array.isEmpty else {
+            return 0
+        }
+        let sorted = array.sorted()
+        let length = array.count
+
+        if length % 2 == 0 {
+            return Double((sorted[length / 2 - 1] + sorted[length / 2]) / 2)
+        }
+        return Double(sorted[length / 2])
+    }
+
+    private func tir(_ array: [Readings]) -> (TIR: Double, hypos: Double, hypers: Double, normal_: Double) {
+        let glucose = array
+        let justGlucoseArray = glucose.compactMap({ each in Int(each.glucose as Int16) })
+        let totalReadings = justGlucoseArray.count
+        let highLimit = settingsManager.settings.high
+        let lowLimit = settingsManager.settings.low
+        let hyperArray = glucose.filter({ $0.glucose >= Int(highLimit) })
+        let hyperReadings = hyperArray.compactMap({ each in each.glucose as Int16 }).count
+        let hyperPercentage = Double(hyperReadings) / Double(totalReadings) * 100
+        let hypoArray = glucose.filter({ $0.glucose <= Int(lowLimit) })
+        let hypoReadings = hypoArray.compactMap({ each in each.glucose as Int16 }).count
+        let hypoPercentage = Double(hypoReadings) / Double(totalReadings) * 100
+        // Euglyccemic range
+        let normalArray = glucose.filter({ $0.glucose >= 70 && $0.glucose <= 140 })
+        let normalReadings = normalArray.compactMap({ each in each.glucose as Int16 }).count
+        let normalPercentage = Double(normalReadings) / Double(totalReadings) * 100
+        // TIR
+        let tir = 100 - (hypoPercentage + hyperPercentage)
+        return (
+            roundDouble(tir, 1),
+            roundDouble(hypoPercentage, 1),
+            roundDouble(hyperPercentage, 1),
+            roundDouble(normalPercentage, 1)
+        )
+    }
+
+    private func glucoseStats(_ fetchedGlucose: [Readings])
+        -> (ifcc: Double, ngsp: Double, average: Double, median: Double, sd: Double, cv: Double, readings: Double)
+    {
+        let glucose = fetchedGlucose
+        // First date
+        let last = glucose.last?.date ?? Date()
+        // Last date (recent)
+        let first = glucose.first?.date ?? Date()
+        // Total time in days
+        let numberOfDays = (first - last).timeInterval / 8.64E4
+        let denominator = numberOfDays < 1 ? 1 : numberOfDays
+        let justGlucoseArray = glucose.compactMap({ each in Int(each.glucose as Int16) })
+        let sumReadings = justGlucoseArray.reduce(0, +)
+        let countReadings = justGlucoseArray.count
+        let glucoseAverage = Double(sumReadings) / Double(countReadings)
+        let medianGlucose = medianCalculation(array: justGlucoseArray)
+        var NGSPa1CStatisticValue = 0.0
+        var IFCCa1CStatisticValue = 0.0
+
+        NGSPa1CStatisticValue = (glucoseAverage + 46.7) / 28.7 // NGSP (%)
+        IFCCa1CStatisticValue = 10.929 *
+            (NGSPa1CStatisticValue - 2.152) // IFCC (mmol/mol)  A1C(mmol/mol) = 10.929 * (A1C(%) - 2.15)
+        var sumOfSquares = 0.0
+
+        for array in justGlucoseArray {
+            sumOfSquares += pow(Double(array) - Double(glucoseAverage), 2)
+        }
+        var sd = 0.0
+        var cv = 0.0
+        // Avoid division by zero
+        if glucoseAverage > 0 {
+            sd = sqrt(sumOfSquares / Double(countReadings))
+            cv = sd / Double(glucoseAverage) * 100
+        }
+        let conversionFactor = 0.0555
+        let units = settingsManager.settings.units
+
+        var output: (ifcc: Double, ngsp: Double, average: Double, median: Double, sd: Double, cv: Double, readings: Double)
+        output = (
+            ifcc: IFCCa1CStatisticValue,
+            ngsp: NGSPa1CStatisticValue,
+            average: glucoseAverage * (units == .mmolL ? conversionFactor : 1),
+            median: medianGlucose * (units == .mmolL ? conversionFactor : 1),
+            sd: sd * (units == .mmolL ? conversionFactor : 1), cv: cv,
+            readings: Double(countReadings) / denominator
+        )
+        return output
+    }
+
+    private func loops(_ fetchedLoops: [LoopStatRecord]) -> Loops {
+        let loops = fetchedLoops
+        // 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
+        let durationAverage = durationArray.reduce(0, +) / Double(durationArrayCount) * 60
+        let medianDuration = medianCalculationDouble(array: durationArray) * 60
+        let max_duration = (durationArray.max() ?? 0) * 60
+        let min_duration = (durationArray.min() ?? 0) * 60
+        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 median_interval = medianCalculationDouble(array: intervalArray)
+        let intervalAverage = intervalArray.reduce(0, +) / Double(count)
+        let maximumInterval = intervalArray.max()
+        let minimumInterval = intervalArray.min()
+        //
+        let output = Loops(
+            loops: Int(loopNr),
+            errors: errorNR,
+            success_rate: roundDecimal(Decimal(successRate ?? 0), 1),
+            avg_interval: roundDecimal(Decimal(intervalAverage), 1),
+            median_interval: roundDecimal(Decimal(median_interval), 1),
+            min_interval: roundDecimal(Decimal(minimumInterval ?? 0), 1),
+            max_interval: roundDecimal(Decimal(maximumInterval ?? 0), 1),
+            avg_duration: roundDecimal(Decimal(durationAverage), 1),
+            median_duration: roundDecimal(Decimal(medianDuration), 1),
+            min_duration: roundDecimal(Decimal(min_duration), 1),
+            max_duration: roundDecimal(Decimal(max_duration), 1)
+        )
+        return output
+    }
+
+    // Add to statistics.JSON for upload to NS.
     private func statistics() {
         let now = Date()
         if settingsManager.settings.uploadStats {
@@ -766,22 +897,21 @@ final class BaseAPSManager: APSManager, Injectable {
                 let units = self.settingsManager.settings.units
                 let preferences = settingsManager.preferences
 
+                // Carbs
                 var carbs = [Carbohydrates]()
                 var carbTotal: Decimal = 0
                 let requestCarbs = Carbohydrates.fetchRequest() as NSFetchRequest<Carbohydrates>
                 let daysAgo = Date().addingTimeInterval(-1.days.timeInterval)
                 requestCarbs.predicate = NSPredicate(format: "carbs > 0 AND date > %@", daysAgo as NSDate)
-
                 let sortCarbs = NSSortDescriptor(key: "date", ascending: true)
                 requestCarbs.sortDescriptors = [sortCarbs]
                 try? carbs = coredataContext.fetch(requestCarbs)
-
                 carbTotal = carbs.map({ carbs in carbs.carbs as? Decimal ?? 0 }).reduce(0, +)
 
+                // TDD
                 var tdds = [TDD]()
                 var currentTDD: Decimal = 0
                 var tddTotalAverage: Decimal = 0
-
                 let requestTDD = TDD.fetchRequest() as NSFetchRequest<TDD>
                 let sort = NSSortDescriptor(key: "timestamp", ascending: false)
                 let daysOf14Ago = Date().addingTimeInterval(-14.days.timeInterval)
@@ -806,7 +936,6 @@ final class BaseAPSManager: APSManager, Injectable {
                 } else if preferences.useNewFormula, !preferences.sigmoid,!preferences.enableDynamicCR {
                     algo_ = "Dynamic ISF: Logarithmic"
                 }
-
                 let af = preferences.adjustmentFactor
                 let insulin_type = preferences.curve
                 let buildDate = Bundle.main.buildDate
@@ -825,284 +954,79 @@ final class BaseAPSManager: APSManager, Injectable {
                 } else if preferences.curve.rawValue == "ultra-rapid" {
                     iPa = 50
                 }
-
-                var lsr = [LoopStatRecord]()
-
-                let requestLSR = LoopStatRecord.fetchRequest() as NSFetchRequest<LoopStatRecord>
-                requestLSR.predicate = NSPredicate(
-                    format: "interval > 0 AND start > %@",
-                    Date().addingTimeInterval(-24.hours.timeInterval) as NSDate
-                )
-                let sortLSR = NSSortDescriptor(key: "start", ascending: false)
-                requestLSR.sortDescriptors = [sortLSR]
-
-                try? lsr = coredataContext.fetch(requestLSR)
-                let loops = lsr
-
-                let durationArray = loops.compactMap({ each in each.duration })
-                let durationArrayCount = durationArray.count
-                let successsNR = loops.compactMap({ each in each.loopStatus }).filter({ each in each!.contains("Success") }).count
-
-                let durationAverage = durationArray.reduce(0, +) / Double(durationArrayCount)
-                let medianDuration = medianCalculation(array: durationArray)
-                let minimumDuration = durationArray.min() ?? 0
-                let maximumDuration = durationArray.max() ?? 0
-                let errorNR = durationArrayCount - successsNR
-                let successRate: Double? = (Double(successsNR) / Double(successsNR + errorNR)) * 100
-                let loopNr = successsNR + errorNR
-
-                let intervalArray = loops.compactMap({ each in each.interval })
-                let intervalArrayCount = intervalArray.count
-                let intervalAverage = intervalArray.reduce(0, +) / Double(intervalArrayCount)
-                let intervalMedian = medianCalculation(array: intervalArray)
-                let maximumInterval = intervalArray.max() ?? 0
-                let minimumInterval = intervalArray.min() ?? 0
-
-                var glucose = [Readings]()
-
-                var firstElementTime = Date()
-                var lastElementTime = Date()
-                var currentIndexTime = Date()
-
-                var bg: Decimal = 0
-
-                var bgArray: [Double] = []
-                var bgArray_1_: [Double] = []
-                var bgArray_7_: [Double] = []
-                var bgArray_30_: [Double] = []
-
-                var bgArrayForTIR: [(bg_: Double, date_: Date)] = []
-                var bgArray_1: [(bg_: Double, date_: Date)] = []
-                var bgArray_7: [(bg_: Double, date_: Date)] = []
-                var bgArray_30: [(bg_: Double, date_: Date)] = []
-
-                var medianBG = 0.0
-                var nr_bgs: Decimal = 0
-                var bg_1: Decimal = 0
-                var bg_7: Decimal = 0
-                var bg_30: Decimal = 0
-                var bg_total: Decimal = 0
-                var j = -1
-                var conversionFactor: Decimal = 1
-                if units == .mmolL {
-                    conversionFactor = 0.0555
-                }
-
-                var numberOfDays: Double = 0
-                var nr1: Decimal = 0
-
+                // CGM Readings
+                var glucose_24 = [Readings]() // Day
+                var glucose_7 = [Readings]() // Week
+                var glucose_30 = [Readings]() // Month
+                var glucose = [Readings]() // Total
+                let filter = DateFilter()
+                // 24h
+                let requestGFS_24 = Readings.fetchRequest() as NSFetchRequest<Readings>
+                let sortGlucose_24 = NSSortDescriptor(key: "date", ascending: false)
+                requestGFS_24.predicate = NSPredicate(format: "glucose > 0 AND date > %@", filter.day)
+                requestGFS_24.sortDescriptors = [sortGlucose_24]
+                try? glucose_24 = coredataContext.fetch(requestGFS_24)
+                // Week
+                let requestGFS_7 = Readings.fetchRequest() as NSFetchRequest<Readings>
+                let sortGlucose_7 = NSSortDescriptor(key: "date", ascending: false)
+                requestGFS_7.predicate = NSPredicate(format: "glucose > 0 AND date > %@", filter.week)
+                requestGFS_7.sortDescriptors = [sortGlucose_7]
+                try? glucose_7 = coredataContext.fetch(requestGFS_7)
+                // Month
+                let requestGFS_30 = Readings.fetchRequest() as NSFetchRequest<Readings>
+                let sortGlucose_30 = NSSortDescriptor(key: "date", ascending: false)
+                requestGFS_30.predicate = NSPredicate(format: "glucose > 0 AND date > %@", filter.month)
+                requestGFS_30.sortDescriptors = [sortGlucose_30]
+                try? glucose_30 = coredataContext.fetch(requestGFS_30)
+                // Total
                 let requestGFS = Readings.fetchRequest() as NSFetchRequest<Readings>
                 let sortGlucose = NSSortDescriptor(key: "date", ascending: false)
+                requestGFS.predicate = NSPredicate(format: "glucose > 0 AND date > %@", filter.total)
                 requestGFS.sortDescriptors = [sortGlucose]
-
                 try? glucose = coredataContext.fetch(requestGFS)
 
-                // Time In Range (%) and Average Glucose. This will be refactored later after some testing.
-                let endIndex = glucose.count - 1
-
-                firstElementTime = glucose[0].date ?? Date()
-                lastElementTime = glucose[endIndex].date ?? Date()
-
-                currentIndexTime = firstElementTime
-
-                numberOfDays = (firstElementTime - lastElementTime).timeInterval / 8.64E4
-
-                // Make arrays for median calculations and calculate averages
-                if endIndex >= 0, (glucose.first?.glucose ?? 0) != 0 {
-                    repeat {
-                        j += 1
-                        if glucose[j].glucose > 0 {
-                            currentIndexTime = glucose[j].date ?? firstElementTime
-                            bg += Decimal(glucose[j].glucose) * conversionFactor
-                            bgArray.append(Double(glucose[j].glucose) * Double(conversionFactor))
-                            bgArrayForTIR.append((Double(glucose[j].glucose), glucose[j].date!))
-                            nr_bgs += 1
-                            if (firstElementTime - currentIndexTime).timeInterval <= 8.64E4 { // 1 day
-                                bg_1 = bg / nr_bgs
-                                bgArray_1 = bgArrayForTIR
-                                bgArray_1_ = bgArray
-                                nr1 = nr_bgs
-                            }
-                            if (firstElementTime - currentIndexTime).timeInterval <= 6.048E5 { // 7 days
-                                bg_7 = bg / nr_bgs
-                                bgArray_7 = bgArrayForTIR
-                                bgArray_7_ = bgArray
-                            }
-                            if (firstElementTime - currentIndexTime).timeInterval <= 2.592E6 { // 30 days
-                                bg_30 = bg / nr_bgs
-                                bgArray_30 = bgArrayForTIR
-                                bgArray_30_ = bgArray
-                            }
-                        }
-                    } while j != glucose.count - 1
-                } else { return }
-
-                if nr_bgs > 0 {
-                    // Up to 91 days
-                    bg_total = bg / nr_bgs
-                }
-
-                // Total median
-                medianBG = medianCalculation(array: bgArray)
-
-                func tir(_ array: [(bg_: Double, date_: Date)]) -> (TIR: Double, hypos: Double, hypers: Double) {
-                    var timeInHypo = 0.0
-                    var timeInHyper = 0.0
-                    var hypos = 0.0
-                    var hypers = 0.0
-                    var i = -1
-                    var lastIndex = false
-                    let endIndex = array.count - 1
-
-                    let hypoLimit = settingsManager.settings.low
-                    let hyperLimit = settingsManager.settings.high
+                // First date
+                let previous = glucose.last?.date ?? Date()
+                // Last date (recent)
+                let current = glucose.first?.date ?? Date()
+                // Total time in days
+                let numberOfDays = (current - previous).timeInterval / 8.64E4
 
-                    var full_time = 0.0
-                    if endIndex > 0 {
-                        full_time = (array[0].date_ - array[endIndex].date_).timeInterval
-                    }
-                    while i < endIndex {
-                        i += 1
-                        let currentTime = array[i].date_
-                        var previousTime = currentTime
-                        if i + 1 <= endIndex {
-                            previousTime = array[i + 1].date_
-                        } else {
-                            lastIndex = true
-                        }
-                        if array[i].bg_ < Double(hypoLimit), !lastIndex {
-                            // Exclude duration between CGM readings which are more than 30 minutes
-                            timeInHypo += min((currentTime - previousTime).timeInterval, 30.minutes.timeInterval)
-                        } else if array[i].bg_ >= Double(hyperLimit), !lastIndex {
-                            timeInHyper += min((currentTime - previousTime).timeInterval, 30.minutes.timeInterval)
-                        }
-                    }
-                    if timeInHypo == 0.0 {
-                        hypos = 0
-                    } else if full_time != 0.0 { hypos = (timeInHypo / full_time) * 100
-                    }
-                    if timeInHyper == 0.0 {
-                        hypers = 0
-                    } else if full_time != 0.0 { hypers = (timeInHyper / full_time) * 100
-                    }
-                    let TIR = 100 - (hypos + hypers)
-                    return (roundDouble(TIR, 1), roundDouble(hypos, 1), roundDouble(hypers, 1))
-                }
-
-                // HbA1c estimation (%, mmol/mol) 1 day
-                var NGSPa1CStatisticValue: Decimal = 0.0
-                var IFCCa1CStatisticValue: Decimal = 0.0
-                if nr_bgs > 0 {
-                    NGSPa1CStatisticValue = ((bg_1 / conversionFactor) + 46.7) / 28.7 // NGSP (%)
-                    IFCCa1CStatisticValue = 10.929 *
-                        (NGSPa1CStatisticValue - 2.152) // IFCC (mmol/mol)  A1C(mmol/mol) = 10.929 * (A1C(%) - 2.15)
-                }
-                // 7 days
-                var NGSPa1CStatisticValue_7: Decimal = 0.0
-                var IFCCa1CStatisticValue_7: Decimal = 0.0
-                if nr_bgs > 0 {
-                    NGSPa1CStatisticValue_7 = ((bg_7 / conversionFactor) + 46.7) / 28.7
-                    IFCCa1CStatisticValue_7 = 10.929 * (NGSPa1CStatisticValue_7 - 2.152)
-                }
-                // 30 days
-                var NGSPa1CStatisticValue_30: Decimal = 0.0
-                var IFCCa1CStatisticValue_30: Decimal = 0.0
-                if nr_bgs > 0 {
-                    NGSPa1CStatisticValue_30 = ((bg_30 / conversionFactor) + 46.7) / 28.7
-                    IFCCa1CStatisticValue_30 = 10.929 * (NGSPa1CStatisticValue_30 - 2.152)
-                }
-                // Total days
-                var NGSPa1CStatisticValue_total: Decimal = 0.0
-                var IFCCa1CStatisticValue_total: Decimal = 0.0
-                if nr_bgs > 0 {
-                    NGSPa1CStatisticValue_total = ((bg_total / conversionFactor) + 46.7) / 28.7
-                    IFCCa1CStatisticValue_total = 10.929 *
-                        (NGSPa1CStatisticValue_total - 2.152)
-                }
+                // Get glucose computations for every case
+                let oneDayGlucose = glucoseStats(glucose_24)
+                let sevenDaysGlucose = glucoseStats(glucose_7)
+                let thirtyDaysGlucose = glucoseStats(glucose_30)
+                let totalDaysGlucose = glucoseStats(glucose)
 
                 let median = Durations(
-                    day: roundDecimal(Decimal(medianCalculation(array: bgArray_1_)), 1),
-                    week: roundDecimal(Decimal(medianCalculation(array: bgArray_7_)), 1),
-                    month: roundDecimal(Decimal(medianCalculation(array: bgArray_30_)), 1),
-                    total: roundDecimal(Decimal(medianBG), 1)
+                    day: roundDecimal(Decimal(oneDayGlucose.median), 1),
+                    week: roundDecimal(Decimal(sevenDaysGlucose.median), 1),
+                    month: roundDecimal(Decimal(thirtyDaysGlucose.median), 1),
+                    total: roundDecimal(Decimal(totalDaysGlucose.median), 1)
                 )
 
-                let saveMedianToCoreData = BGmedian(context: self.coredataContext)
-                saveMedianToCoreData.date = Date()
-                saveMedianToCoreData.median = median.total as NSDecimalNumber
-                saveMedianToCoreData.median_1 = median.day as NSDecimalNumber
-                saveMedianToCoreData.median_7 = median.week as NSDecimalNumber
-                saveMedianToCoreData.median_30 = median.month as NSDecimalNumber
-
-                try? self.coredataContext.save()
-
-                var hbs = Durations(
-                    day: roundDecimal(NGSPa1CStatisticValue, 1),
-                    week: roundDecimal(NGSPa1CStatisticValue_7, 1),
-                    month: roundDecimal(NGSPa1CStatisticValue_30, 1),
-                    total: roundDecimal(NGSPa1CStatisticValue_total, 1)
-                )
-
-                let saveHbA1c = HbA1c(context: self.coredataContext)
-                saveHbA1c.date = Date()
-                saveHbA1c.hba1c = NGSPa1CStatisticValue_total as NSDecimalNumber
-                saveHbA1c.hba1c_1 = NGSPa1CStatisticValue as NSDecimalNumber
-                saveHbA1c.hba1c_7 = NGSPa1CStatisticValue_7 as NSDecimalNumber
-                saveHbA1c.hba1c_30 = NGSPa1CStatisticValue_30 as NSDecimalNumber
-
-                try? self.coredataContext.save()
-
-                // Convert to user-preferred unit
                 let overrideHbA1cUnit = settingsManager.settings.overrideHbA1cUnit
-                if units == .mmolL {
-                    // Override if users sets overrideHbA1cUnit: true
-                    if !overrideHbA1cUnit {
-                        hbs = Durations(
-                            day: roundDecimal(IFCCa1CStatisticValue, 1),
-                            week: roundDecimal(IFCCa1CStatisticValue_7, 1),
-                            month: roundDecimal(IFCCa1CStatisticValue_30, 1),
-                            total: roundDecimal(IFCCa1CStatisticValue_total, 1)
-                        )
-                    }
-                } else if units != .mmolL, overrideHbA1cUnit {
-                    hbs = Durations(
-                        day: roundDecimal(IFCCa1CStatisticValue, 1),
-                        week: roundDecimal(IFCCa1CStatisticValue_7, 1),
-                        month: roundDecimal(IFCCa1CStatisticValue_30, 1),
-                        total: roundDecimal(IFCCa1CStatisticValue_total, 1)
-                    )
-                }
-
-                let nrOfCGMReadings = nr1
 
-                let loopstat = LoopCycles(
-                    loops: loopNr,
-                    errors: errorNR,
-                    readings: Int(nrOfCGMReadings),
-                    success_rate: Decimal(round(successRate ?? 0)),
-                    avg_interval: roundDecimal(Decimal(intervalAverage), 1),
-                    median_interval: roundDecimal(Decimal(intervalMedian), 1),
-                    min_interval: roundDecimal(Decimal(minimumInterval), 1),
-                    max_interval: roundDecimal(Decimal(maximumInterval), 1),
-                    avg_duration: Decimal(roundDouble(durationAverage, 2)),
-                    median_duration: Decimal(roundDouble(medianDuration, 2)),
-                    min_duration: roundDecimal(Decimal(minimumDuration), 2),
-                    max_duration: Decimal(roundDouble(maximumDuration, 1))
+                let hbs = Durations(
+                    day: ((units == .mmolL && !overrideHbA1cUnit) || (units == .mgdL && overrideHbA1cUnit)) ?
+                        roundDecimal(Decimal(oneDayGlucose.ifcc), 1) : roundDecimal(Decimal(oneDayGlucose.ngsp), 1),
+                    week: ((units == .mmolL && !overrideHbA1cUnit) || (units == .mgdL && overrideHbA1cUnit)) ?
+                        roundDecimal(Decimal(sevenDaysGlucose.ifcc), 1) : roundDecimal(Decimal(sevenDaysGlucose.ngsp), 1),
+                    month: ((units == .mmolL && !overrideHbA1cUnit) || (units == .mgdL && overrideHbA1cUnit)) ?
+                        roundDecimal(Decimal(thirtyDaysGlucose.ifcc), 1) : roundDecimal(Decimal(thirtyDaysGlucose.ngsp), 1),
+                    total: ((units == .mmolL && !overrideHbA1cUnit) || (units == .mgdL && overrideHbA1cUnit)) ?
+                        roundDecimal(Decimal(totalDaysGlucose.ifcc), 1) : roundDecimal(Decimal(totalDaysGlucose.ngsp), 1)
                 )
 
-                // TIR calcs for every case
-                var oneDay_: (TIR: Double, hypos: Double, hypers: Double) = (0.0, 0.0, 0.0)
-                var sevenDays_: (TIR: Double, hypos: Double, hypers: Double) = (0.0, 0.0, 0.0)
-                var thirtyDays_: (TIR: Double, hypos: Double, hypers: Double) = (0.0, 0.0, 0.0)
-                var totalDays_: (TIR: Double, hypos: Double, hypers: Double) = (0.0, 0.0, 0.0)
-
-                // Get all TIR calcs for every case
-                if nr_bgs > 0 {
-                    oneDay_ = tir(bgArray_1)
-                    sevenDays_ = tir(bgArray_7)
-                    thirtyDays_ = tir(bgArray_30)
-                    totalDays_ = tir(bgArrayForTIR)
-                }
+                var oneDay_: (TIR: Double, hypos: Double, hypers: Double, normal_: Double) = (0.0, 0.0, 0.0, 0.0)
+                var sevenDays_: (TIR: Double, hypos: Double, hypers: Double, normal_: Double) = (0.0, 0.0, 0.0, 0.0)
+                var thirtyDays_: (TIR: Double, hypos: Double, hypers: Double, normal_: Double) = (0.0, 0.0, 0.0, 0.0)
+                var totalDays_: (TIR: Double, hypos: Double, hypers: Double, normal_: Double) = (0.0, 0.0, 0.0, 0.0)
+                // Get TIR computations for every case
+                oneDay_ = tir(glucose_24)
+                sevenDays_ = tir(glucose_7)
+                thirtyDays_ = tir(glucose_30)
+                totalDays_ = tir(glucose)
 
                 let tir = Durations(
                     day: roundDecimal(Decimal(oneDay_.TIR), 1),
@@ -1110,53 +1034,89 @@ final class BaseAPSManager: APSManager, Injectable {
                     month: roundDecimal(Decimal(thirtyDays_.TIR), 1),
                     total: roundDecimal(Decimal(totalDays_.TIR), 1)
                 )
-
                 let hypo = Durations(
                     day: Decimal(oneDay_.hypos),
                     week: Decimal(sevenDays_.hypos),
                     month: Decimal(thirtyDays_.hypos),
                     total: Decimal(totalDays_.hypos)
                 )
-
                 let hyper = Durations(
                     day: Decimal(oneDay_.hypers),
                     week: Decimal(sevenDays_.hypers),
                     month: Decimal(thirtyDays_.hypers),
                     total: Decimal(totalDays_.hypers)
                 )
-
+                let normal = Durations(
+                    day: Decimal(oneDay_.normal_),
+                    week: Decimal(sevenDays_.normal_),
+                    month: Decimal(thirtyDays_.normal_),
+                    total: Decimal(totalDays_.normal_)
+                )
                 let range = Threshold(
                     low: units == .mmolL ? roundDecimal(settingsManager.settings.low.asMmolL, 1) :
                         roundDecimal(settingsManager.settings.low, 0),
                     high: units == .mmolL ? roundDecimal(settingsManager.settings.high.asMmolL, 1) :
                         roundDecimal(settingsManager.settings.high, 0)
                 )
-
                 let TimeInRange = TIRs(
                     TIR: tir,
                     Hypos: hypo,
                     Hypers: hyper,
-                    Threshold: range
+                    Threshold: range,
+                    Euglycemic: normal
                 )
-
                 let avgs = Durations(
-                    day: roundDecimal(bg_1, 1),
-                    week: roundDecimal(bg_7, 1),
-                    month: roundDecimal(bg_30, 1),
-                    total: roundDecimal(bg_total, 1)
+                    day: roundDecimal(Decimal(oneDayGlucose.average), 1),
+                    week: roundDecimal(Decimal(sevenDaysGlucose.average), 1),
+                    month: roundDecimal(Decimal(thirtyDaysGlucose.average), 1),
+                    total: roundDecimal(Decimal(totalDaysGlucose.average), 1)
                 )
+                let avg = Averages(Average: avgs, Median: median)
+                // Standard Deviations
+                let standardDeviations = Durations(
+                    day: roundDecimal(Decimal(oneDayGlucose.sd), 1),
+                    week: roundDecimal(Decimal(sevenDaysGlucose.sd), 1),
+                    month: roundDecimal(Decimal(thirtyDaysGlucose.sd), 1),
+                    total: roundDecimal(Decimal(totalDaysGlucose.sd), 1)
+                )
+                // CV = standard deviation / sample mean x 100
+                let cvs = Durations(
+                    day: roundDecimal(Decimal(oneDayGlucose.cv), 1),
+                    week: roundDecimal(Decimal(sevenDaysGlucose.cv), 1),
+                    month: roundDecimal(Decimal(thirtyDaysGlucose.cv), 1),
+                    total: roundDecimal(Decimal(totalDaysGlucose.cv), 1)
+                )
+                let variance = Variance(SD: standardDeviations, CV: cvs)
 
-                let saveAverages = BGaverages(context: self.coredataContext)
-                saveAverages.date = Date()
-                saveAverages.average = bg_total as NSDecimalNumber
-                saveAverages.average_1 = bg_1 as NSDecimalNumber
-                saveAverages.average_7 = bg_7 as NSDecimalNumber
-                saveAverages.average_30 = bg_30 as NSDecimalNumber
-                try? self.coredataContext.save()
+                // Loops
+                var lsr = [LoopStatRecord]()
+                let requestLSR = LoopStatRecord.fetchRequest() as NSFetchRequest<LoopStatRecord>
+                requestLSR.predicate = NSPredicate(
+                    format: "interval > 0 AND start > %@",
+                    Date().addingTimeInterval(-24.hours.timeInterval) as NSDate
+                )
+                let sortLSR = NSSortDescriptor(key: "start", ascending: false)
+                requestLSR.sortDescriptors = [sortLSR]
+                try? lsr = coredataContext.fetch(requestLSR)
+                // Compute LoopStats for 24 hours
+                let oneDayLoops = loops(lsr)
+                let loopstat = LoopCycles(
+                    loops: oneDayLoops.loops,
+                    errors: oneDayLoops.errors,
+                    readings: Int(oneDayGlucose.readings),
+                    success_rate: oneDayLoops.success_rate,
+                    avg_interval: oneDayLoops.avg_interval,
+                    median_interval: oneDayLoops.median_interval,
+                    min_interval: oneDayLoops.min_interval,
+                    max_interval: oneDayLoops.max_interval,
+                    avg_duration: oneDayLoops.avg_duration,
+                    median_duration: oneDayLoops.median_duration,
+                    min_duration: oneDayLoops.max_duration,
+                    max_duration: oneDayLoops.max_duration
+                )
 
-                let avg = Averages(Average: avgs, Median: median)
+                // Insulin
                 var insulinDistribution = [InsulinDistribution]()
-
                 var insulin = Ins(
                     TDD: 0,
                     bolus: 0,
@@ -1164,13 +1124,10 @@ final class BaseAPSManager: APSManager, Injectable {
                     scheduled_basal: 0,
                     total_average: 0
                 )
-
                 let requestInsulinDistribution = InsulinDistribution.fetchRequest() as NSFetchRequest<InsulinDistribution>
                 let sortInsulin = NSSortDescriptor(key: "date", ascending: false)
                 requestInsulinDistribution.sortDescriptors = [sortInsulin]
-
                 try? insulinDistribution = coredataContext.fetch(requestInsulinDistribution)
-
                 insulin = Ins(
                     TDD: roundDecimal(currentTDD, 2),
                     bolus: insulinDistribution.first != nil ? ((insulinDistribution.first?.bolus ?? 0) as Decimal) : 0,
@@ -1180,73 +1137,7 @@ final class BaseAPSManager: APSManager, Injectable {
                     total_average: roundDecimal(tddTotalAverage, 1)
                 )
 
-                var sumOfSquares = 0.0
-                var sumOfSquares_1 = 0.0
-                var sumOfSquares_7 = 0.0
-                var sumOfSquares_30 = 0.0
-
-                // Total
-                for array in bgArray {
-                    sumOfSquares += pow(array - Double(bg_total), 2)
-                }
-                // One day
-                for array_1 in bgArray_1_ {
-                    sumOfSquares_1 += pow(array_1 - Double(bg_1), 2)
-                }
-                // week
-                for array_7 in bgArray_7_ {
-                    sumOfSquares_7 += pow(array_7 - Double(bg_7), 2)
-                }
-                // month
-                for array_30 in bgArray_30_ {
-                    sumOfSquares_30 += pow(array_30 - Double(bg_30), 2)
-                }
-
-                // Standard deviation and Coefficient of variation
-                var sd_total = 0.0
-                var cv_total = 0.0
-                var sd_1 = 0.0
-                var cv_1 = 0.0
-                var sd_7 = 0.0
-                var cv_7 = 0.0
-                var sd_30 = 0.0
-                var cv_30 = 0.0
-
-                // Avoid division by zero
-                if bg_total > 0 {
-                    sd_total = sqrt(sumOfSquares / Double(nr_bgs))
-                    cv_total = sd_total / Double(bg_total) * 100
-                }
-                if bg_1 > 0 {
-                    sd_1 = sqrt(sumOfSquares_1 / Double(bgArray_1_.count))
-                    cv_1 = sd_1 / Double(bg_1) * 100
-                }
-                if bg_7 > 0 {
-                    sd_7 = sqrt(sumOfSquares_7 / Double(bgArray_7_.count))
-                    cv_7 = sd_7 / Double(bg_7) * 100
-                }
-                if bg_30 > 0 {
-                    sd_30 = sqrt(sumOfSquares_30 / Double(bgArray_30_.count))
-                    cv_30 = sd_30 / Double(bg_30) * 100
-                }
-
-                // Standard Deviations
-                let standardDeviations = Durations(
-                    day: roundDecimal(Decimal(sd_1), 1),
-                    week: roundDecimal(Decimal(sd_7), 1),
-                    month: roundDecimal(Decimal(sd_30), 1),
-                    total: roundDecimal(Decimal(sd_total), 1)
-                )
-
-                // CV = standard deviation / sample mean x 100
-                let cvs = Durations(
-                    day: roundDecimal(Decimal(cv_1), 1),
-                    week: roundDecimal(Decimal(cv_7), 1),
-                    month: roundDecimal(Decimal(cv_30), 1),
-                    total: roundDecimal(Decimal(cv_total), 1)
-                )
-
-                let variance = Variance(SD: standardDeviations, CV: cvs)
+                let hbA1cUnit = !overrideHbA1cUnit ? (units == .mmolL ? "mmol/mol" : "%") : (units == .mmolL ? "%" : "mmol/mol")
 
                 let dailystat = Statistics(
                     created_at: Date(),
@@ -1268,13 +1159,12 @@ final class BaseAPSManager: APSManager, Injectable {
                     Statistics: Stats(
                         Distribution: TimeInRange,
                         Glucose: avg,
-                        HbA1c: hbs,
+                        HbA1c: hbs, Units: Units(Glucose: units.rawValue, HbA1c: hbA1cUnit),
                         LoopCycles: loopstat,
                         Insulin: insulin,
                         Variance: variance
                     )
                 )
-
                 storage.save(dailystat, as: file)
                 nightscout.uploadStatistics(dailystat: dailystat)
                 nightscout.uploadPreferences()

+ 85 - 11
FreeAPS/Sources/APS/Storage/CarbsStorage.swift

@@ -19,6 +19,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
     private let processQueue = DispatchQueue(label: "BaseCarbsStorage.processQueue")
     @Injected() private var storage: FileStorage!
     @Injected() private var broadcaster: Broadcaster!
+    @Injected() private var settings: SettingsManager!
 
     let coredataContext = CoreDataStack.shared.persistentContainer.newBackgroundContext()
 
@@ -26,25 +27,97 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         injectServices(resolver)
     }
 
-    func storeCarbs(_ carbs: [CarbsEntry]) {
+    func storeCarbs(_ entries: [CarbsEntry]) {
         processQueue.sync {
             let file = OpenAPS.Monitor.carbHistory
             var uniqEvents: [CarbsEntry] = []
-            self.storage.transaction { storage in
-                storage.append(carbs, to: file, uniqBy: \.createdAt)
-                uniqEvents = storage.retrieve(file, as: [CarbsEntry].self)?
-                    .filter { $0.createdAt.addingTimeInterval(1.days.timeInterval) > Date() }
-                    .sorted { $0.createdAt > $1.createdAt } ?? []
-                storage.save(Array(uniqEvents), as: file)
+
+            let fat = entries.last?.fat ?? 0
+            let protein = entries.last?.protein ?? 0
+
+            if fat > 0 || protein > 0 {
+                // -------------------------- FPU--------------------------------------
+                let interval = settings.settings.minuteInterval // Interval betwwen carbs
+                let timeCap = settings.settings.timeCap // Max Duration
+                let adjustment = settings.settings.individualAdjustmentFactor
+                let delay = settings.settings.delay // Tme before first future carb entry
+                let kcal = protein * 4 + fat * 9
+                let carbEquivalents = (kcal / 10) * adjustment
+                let fpus = carbEquivalents / 10
+                // Duration in hours used for extended boluses with Warsaw Method. Here used for total duration of the computed carbquivalents instead, excluding the configurable delay.
+                var computedDuration = 0
+                switch fpus {
+                case ..<2:
+                    computedDuration = 3
+                case 2 ..< 3:
+                    computedDuration = 4
+                case 3 ..< 4:
+                    computedDuration = 5
+                default:
+                    computedDuration = timeCap
+                }
+                // Size of each created carb equivalent if 60 minutes interval
+                var equivalent: Decimal = carbEquivalents / Decimal(computedDuration)
+                // Adjust for interval setting other than 60 minutes
+                equivalent /= Decimal(60 / interval)
+                // Round to 1 fraction digit
+                // equivalent = Decimal(round(Double(equivalent * 10) / 10))
+                let roundedEquivalent: Double = round(Double(equivalent * 10)) / 10
+                equivalent = Decimal(roundedEquivalent)
+                // Number of equivalents
+                var numberOfEquivalents = carbEquivalents / equivalent
+                // Only use delay in first loop
+                var firstIndex = true
+                // New date for each carb equivalent
+                var useDate = entries.last?.createdAt ?? Date()
+                // Group and Identify all FPUs together
+                let fpuID = UUID().uuidString
+                // Create an array of all future carb equivalents.
+                var futureCarbArray = [CarbsEntry]()
+                while carbEquivalents > 0, numberOfEquivalents > 0 {
+                    if firstIndex {
+                        useDate = useDate.addingTimeInterval(delay.minutes.timeInterval)
+                        firstIndex = false
+                    } else { useDate = useDate.addingTimeInterval(interval.minutes.timeInterval) }
+
+                    let eachCarbEntry = CarbsEntry(
+                        id: UUID().uuidString, createdAt: useDate, carbs: equivalent, fat: 0, protein: 0,
+                        enteredBy: CarbsEntry.manual, isFPU: true,
+                        fpuID: fpuID
+                    )
+                    futureCarbArray.append(eachCarbEntry)
+                    numberOfEquivalents -= 1
+                }
+                // Save the array
+                if carbEquivalents > 0 {
+                    self.storage.transaction { storage in
+                        storage.append(futureCarbArray, to: file, uniqBy: \.createdAt)
+                        uniqEvents = storage.retrieve(file, as: [CarbsEntry].self)?
+                            .filter { $0.createdAt.addingTimeInterval(1.days.timeInterval) > Date() }
+                            .sorted { $0.createdAt > $1.createdAt } ?? []
+                        storage.save(Array(uniqEvents), as: file)
+                    }
+                }
+            } // ------------------------- END OF TPU ----------------------------------------
+            // Store the actual (normal) carbs
+            if entries.last?.carbs ?? 0 > 0 {
+                uniqEvents = []
+                self.storage.transaction { storage in
+                    storage.append(entries, to: file, uniqBy: \.createdAt)
+                    uniqEvents = storage.retrieve(file, as: [CarbsEntry].self)?
+                        .filter { $0.createdAt.addingTimeInterval(1.days.timeInterval) > Date() }
+                        .sorted { $0.createdAt > $1.createdAt } ?? []
+                    storage.save(Array(uniqEvents), as: file)
+                }
             }
 
             // MARK: Save to CoreData. TEST
 
             var cbs: Decimal = 0
             var carbDate = Date()
-            if carbs.isNotEmpty {
-                cbs = carbs[0].carbs
-                carbDate = carbs[0].createdAt
+            if entries.isNotEmpty {
+                cbs = entries[0].carbs
+                carbDate = entries[0].createdAt
             }
             if cbs != 0 {
                 self.coredataContext.perform {
@@ -56,7 +129,6 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
                     try? self.coredataContext.save()
                 }
             }
-
             broadcaster.notify(CarbsObserver.self, on: processQueue) {
                 $0.carbsDidUpdate(uniqEvents)
             }
@@ -115,6 +187,8 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
                 insulin: nil,
                 notes: nil,
                 carbs: $0.carbs,
+                fat: nil,
+                protein: nil,
                 targetTop: nil,
                 targetBottom: nil
             )

+ 2 - 0
FreeAPS/Sources/APS/Storage/GlucoseStorage.swift

@@ -117,6 +117,8 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                         insulin: nil,
                         notes: notes,
                         carbs: nil,
+                        fat: nil,
+                        protein: nil,
                         targetTop: nil,
                         targetBottom: nil
                     )

+ 12 - 0
FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift

@@ -230,6 +230,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                     insulin: nil,
                     notes: nil,
                     carbs: nil,
+                    fat: nil,
+                    protein: nil,
                     targetTop: nil,
                     targetBottom: nil
                 ))
@@ -260,6 +262,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                     insulin: event.amount,
                     notes: nil,
                     carbs: nil,
+                    fat: nil,
+                    protein: nil,
                     targetTop: nil,
                     targetBottom: nil
                 )
@@ -277,6 +281,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                     insulin: nil,
                     notes: nil,
                     carbs: Decimal(event.carbInput ?? 0),
+                    fat: nil,
+                    protein: nil,
                     targetTop: nil,
                     targetBottom: nil
                 )
@@ -300,6 +306,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                     insulin: nil,
                     notes: nil,
                     carbs: nil,
+                    fat: nil,
+                    protein: nil,
                     targetTop: nil,
                     targetBottom: nil
                 )
@@ -317,6 +325,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                     insulin: nil,
                     notes: nil,
                     carbs: nil,
+                    fat: nil,
+                    protein: nil,
                     targetTop: nil,
                     targetBottom: nil
                 )
@@ -334,6 +344,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                     insulin: nil,
                     notes: "Alarm \(String(describing: event.note)) \(event.type)",
                     carbs: nil,
+                    fat: nil,
+                    protein: nil,
                     targetTop: nil,
                     targetBottom: nil
                 )

+ 1 - 0
FreeAPS/Sources/Helpers/Color+Extensions.swift

@@ -62,4 +62,5 @@ extension Color {
     static let loopPink = Color("LoopPink")
     static let lemon = Color("Lemon")
     static let minus = Color("minus")
+    static let darkGray = Color("darkGray")
 }

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 4
FreeAPS/Sources/Localizations/Main/ar.lproj/Localizable.strings


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 3
FreeAPS/Sources/Localizations/Main/ca.lproj/Localizable.strings


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 4
FreeAPS/Sources/Localizations/Main/da.lproj/Localizable.strings


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 4
FreeAPS/Sources/Localizations/Main/de.lproj/Localizable.strings


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 4
FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 4
FreeAPS/Sources/Localizations/Main/es.lproj/Localizable.strings


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 4
FreeAPS/Sources/Localizations/Main/fi.lproj/Localizable.strings


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 4
FreeAPS/Sources/Localizations/Main/fr.lproj/Localizable.strings


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 4
FreeAPS/Sources/Localizations/Main/he.lproj/Localizable.strings


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 1
FreeAPS/Sources/Localizations/Main/it.lproj/Localizable.strings


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 1
FreeAPS/Sources/Localizations/Main/nb.lproj/Localizable.strings


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 4
FreeAPS/Sources/Localizations/Main/nl.lproj/Localizable.strings


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 4
FreeAPS/Sources/Localizations/Main/pl.lproj/Localizable.strings


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 4
FreeAPS/Sources/Localizations/Main/pt-BR.lproj/Localizable.strings


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 4
FreeAPS/Sources/Localizations/Main/pt-PT.lproj/Localizable.strings


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 4
FreeAPS/Sources/Localizations/Main/ru.lproj/Localizable.strings


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 4
FreeAPS/Sources/Localizations/Main/sk.lproj/Localizable.strings


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 4
FreeAPS/Sources/Localizations/Main/sv.lproj/Localizable.strings


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 4
FreeAPS/Sources/Localizations/Main/tr.lproj/Localizable.strings


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 7
FreeAPS/Sources/Localizations/Main/uk.lproj/Localizable.strings


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 4
FreeAPS/Sources/Localizations/Main/zh-Hans.lproj/Localizable.strings


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

@@ -4,6 +4,8 @@ struct CarbsEntry: JSON, Equatable, Hashable {
     let id: String?
     let createdAt: Date
     let carbs: Decimal
+    let fat: Decimal?
+    let protein: Decimal?
     let enteredBy: String?
     let isFPU: Bool?
     let fpuID: String?
@@ -25,6 +27,8 @@ extension CarbsEntry {
         case id = "_id"
         case createdAt = "created_at"
         case carbs
+        case fat
+        case protein
         case enteredBy
         case isFPU
         case fpuID

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

@@ -41,6 +41,7 @@ struct FreeAPSSettings: JSON, Equatable {
     var oneDimensionalGraph: Bool = false
     var rulerMarks: Bool = false
     var maxCarbs: Decimal = 1000
+    var displayFatAndProteinOnWatch: Bool = false
 }
 
 extension FreeAPSSettings: Decodable {
@@ -214,6 +215,10 @@ extension FreeAPSSettings: Decodable {
             settings.maxCarbs = maxCarbs
         }
 
+        if let displayFatAndProteinOnWatch = try? container.decode(Bool.self, forKey: .displayFatAndProteinOnWatch) {
+            settings.displayFatAndProteinOnWatch = displayFatAndProteinOnWatch
+        }
+
         self = settings
     }
 }

+ 15 - 0
FreeAPS/Sources/Models/Loops.swift

@@ -0,0 +1,15 @@
+import Foundation
+
+struct Loops: JSON, Equatable {
+    var loops: Int
+    var errors: Int
+    var success_rate: Decimal
+    var avg_interval: Decimal
+    var median_interval: Decimal
+    var min_interval: Decimal
+    var max_interval: Decimal
+    var avg_duration: Decimal
+    var median_duration: Decimal
+    var min_duration: Decimal
+    var max_duration: Decimal
+}

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

@@ -13,6 +13,8 @@ struct NigtscoutTreatment: JSON, Hashable, Equatable {
     var insulin: Decimal?
     var notes: String?
     var carbs: Decimal?
+    var fat: Decimal?
+    var protein: Decimal?
     let targetTop: Decimal?
     let targetBottom: Decimal?
 
@@ -43,6 +45,8 @@ extension NigtscoutTreatment {
         case insulin
         case notes
         case carbs
+        case fat
+        case protein
         case targetTop
         case targetBottom
     }

+ 9 - 0
FreeAPS/Sources/Models/Statistics.swift

@@ -115,6 +115,11 @@ struct Durations: JSON, Equatable {
     var total: Decimal
 }
 
+struct Units: JSON, Equatable {
+    var Glucose: String
+    var HbA1c: String
+}
+
 struct Threshold: JSON, Equatable {
     var low: Decimal
     var high: Decimal
@@ -125,6 +130,7 @@ struct TIRs: JSON, Equatable {
     var Hypos: Durations
     var Hypers: Durations
     var Threshold: Threshold
+    var Euglycemic: Durations
 }
 
 struct Ins: JSON, Equatable {
@@ -144,6 +150,7 @@ struct Stats: JSON, Equatable {
     var Distribution: TIRs
     var Glucose: Averages
     var HbA1c: Durations
+    var Units: Units
     var LoopCycles: LoopCycles
     var Insulin: Ins
     var Variance: Variance
@@ -179,6 +186,7 @@ extension TIRs {
         case Hypos
         case Hypers
         case Threshold
+        case Euglycemic
     }
 }
 
@@ -204,6 +212,7 @@ extension Stats {
         case Distribution
         case Glucose
         case HbA1c
+        case Units
         case LoopCycles
         case Insulin
         case Variance

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

@@ -28,6 +28,7 @@ struct Suggestion: JSON, Equatable {
     let expectedDelta: Decimal?
     let minGuardBG: Decimal?
     let minPredBG: Decimal?
+    let threshold: Decimal?
 }
 
 struct Predictions: JSON, Equatable {
@@ -73,6 +74,7 @@ extension Suggestion {
         case expectedDelta
         case minGuardBG
         case minPredBG
+        case threshold
     }
 }
 

+ 11 - 73
FreeAPS/Sources/Modules/AddCarbs/AddCarbsStateModel.swift

@@ -33,79 +33,17 @@ extension AddCarbs {
             }
             carbs = min(carbs, maxCarbs)
 
-            if useFPUconversion {
-                // -------------------------- FPU--------------------------------------
-                let interval = settings.settings.minuteInterval // Interval betwwen carbs
-                let timeCap = settings.settings.timeCap // Max Duration
-                let adjustment = settings.settings.individualAdjustmentFactor
-                let delay = settings.settings.delay // Tme before first future carb entry
-
-                let kcal = protein * 4 + fat * 9
-                let carbEquivalents = (kcal / 10) * adjustment
-                let fpus = carbEquivalents / 10
-
-                // Duration in hours used for extended boluses with Warsaw Method. Here used for total duration of the computed carbquivalents instead, excluding the configurable delay.
-                var computedDuration = 0
-                switch fpus {
-                case ..<2:
-                    computedDuration = 3
-                case 2 ..< 3:
-                    computedDuration = 4
-                case 3 ..< 4:
-                    computedDuration = 5
-                default:
-                    computedDuration = timeCap
-                }
-
-                // Size of each created carb equivalent if 60 minutes interval
-                var equivalent: Decimal = carbEquivalents / Decimal(computedDuration)
-                // Adjust for interval setting other than 60 minutes
-                equivalent /= Decimal(60 / interval)
-                // Round to 1 fraction digit
-                // equivalent = Decimal(round(Double(equivalent * 10) / 10))
-                let roundedEquivalent: Double = round(Double(equivalent * 10)) / 10
-                equivalent = Decimal(roundedEquivalent)
-                // Number of equivalents
-                var numberOfEquivalents = carbEquivalents / equivalent
-                // Only use delay in first loop
-                var firstIndex = true
-                // New date for each carb equivalent
-                var useDate = date
-                // Group and Identify all FPUs together
-                let fpuID = UUID().uuidString
-
-                // Create an array of all future carb equivalents.
-                var futureCarbArray = [CarbsEntry]()
-                while carbEquivalents > 0, numberOfEquivalents > 0 {
-                    if firstIndex {
-                        useDate = useDate.addingTimeInterval(delay.minutes.timeInterval)
-                        firstIndex = false
-                    } else { useDate = useDate.addingTimeInterval(interval.minutes.timeInterval) }
-
-                    let eachCarbEntry = CarbsEntry(
-                        id: UUID().uuidString, createdAt: useDate, carbs: equivalent, enteredBy: CarbsEntry.manual, isFPU: true,
-                        fpuID: fpuID
-                    )
-                    futureCarbArray.append(eachCarbEntry)
-                    numberOfEquivalents -= 1
-                }
-                // Save the array
-                if carbEquivalents > 0 {
-                    carbsStorage.storeCarbs(futureCarbArray)
-                }
-            } // ------------------------- END OF TPU ----------------------------------------
-
-            // Store the real carbs
-            if carbs > 0 {
-                carbsStorage
-                    .storeCarbs([CarbsEntry(
-                        id: UUID().uuidString,
-                        createdAt: date,
-                        carbs: carbs,
-                        enteredBy: CarbsEntry.manual,
-                        isFPU: false, fpuID: nil
-                    )])
-            }
+            carbsStorage.storeCarbs(
+                [CarbsEntry(
+                    id: UUID().uuidString,
+                    createdAt: date,
+                    carbs: carbs,
+                    fat: fat,
+                    protein: protein,
+                    enteredBy: CarbsEntry.manual,
+                    isFPU: false, fpuID: nil
+                )]
+            )
 
             if settingsManager.settings.skipBolusScreenAfterCarbs {
                 apsManager.determineBasalSync()

+ 1 - 2
FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift

@@ -33,8 +33,7 @@ extension Bolus {
             broadcaster.register(SuggestionObserver.self, observer: self)
             units = settingsManager.settings.units
             percentage = settingsManager.settings.insulinReqPercentage
-            threshold = units == .mmolL ? settingsManager.preferences.threshold_setting.asMmolL : settingsManager.preferences
-                .threshold_setting
+            threshold = provider.suggestion?.threshold ?? 0
 
             if waitForSuggestionInitial {
                 apsManager.determineBasal()

+ 1 - 1
FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift

@@ -248,7 +248,7 @@ extension Bolus {
                         "which is below your Threshold (",
                         comment: "Bolus pop-up / Alert string. Make translations concise!"
                     ) + state
-                    .threshold.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) + ")"
+                    .threshold.formatted() + " " + state.units.rawValue + ")"
             case 3:
                 return NSLocalizedString(
                     "Eventual Glucose > Target Glucose, but glucose is climbing slower than expected. Expected: ",

+ 0 - 13
FreeAPS/Sources/Modules/CGM/CGMStateModel.swift

@@ -28,18 +28,6 @@ extension CGM {
             cgmTransmitterDeviceAddress = UserDefaults.standard.cgmTransmitterDeviceAddress
 
             subscribeSetting(\.useCalendar, on: $createCalendarEvents) { createCalendarEvents = $0 }
-            subscribeSetting(\.uploadGlucose, on: $uploadGlucose, initial: { uploadGlucose = $0 }, didSet: { val in
-                if let cgmManagerG5 = self.cgmManager.glucoseSource.cgmManager as? G5CGMManager {
-                    cgmManagerG5.shouldSyncToRemoteService = val
-                }
-                if let cgmManagerG6 = self.cgmManager.glucoseSource.cgmManager as? G6CGMManager {
-                    cgmManagerG6.shouldSyncToRemoteService = val
-                }
-                if let cgmManagerG7 = self.cgmManager.glucoseSource.cgmManager as? G7CGMManager {
-                    cgmManagerG7.uploadReadings = val
-                }
-            })
-
             subscribeSetting(\.smoothGlucose, on: $smoothGlucose, initial: { smoothGlucose = $0 })
 
             $cgm
@@ -91,7 +79,6 @@ extension CGM.StateModel: CompletionDelegate {
         }
         // refresh the upload options
         uploadGlucose = settingsManager.settings.uploadGlucose
-
         cgmManager.updateGlucoseSource()
     }
 }

+ 0 - 4
FreeAPS/Sources/Modules/CGM/View/CGMRootView.swift

@@ -64,10 +64,6 @@ extension CGM {
                         }
                     }
 
-                    Section(header: Text("Other")) {
-                        Toggle("Upload glucose to Nightscout", isOn: $state.uploadGlucose)
-                    }
-
                     Section(header: Text("Experimental")) {
                         Toggle("Smooth Glucose Value", isOn: $state.smoothGlucose)
                     }

+ 1 - 1
FreeAPS/Sources/Modules/CREditor/CREditorStateModel.swift

@@ -7,7 +7,7 @@ extension CREditor {
 
         let timeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
 
-        let rateValues = stride(from: 30.0, to: 501.0, by: 1.0).map { ($0.decimal ?? .zero) / 10 }
+        let rateValues = stride(from: 15.0, to: 501.0, by: 1.0).map { ($0.decimal ?? .zero) / 10 }
 
         var canAdd: Bool {
             guard let lastItem = items.last else { return true }

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

@@ -1,4 +1,6 @@
+import CGMBLEKit
 import Combine
+import G7SensorKit
 import SwiftDate
 import SwiftUI
 
@@ -8,14 +10,16 @@ extension NightscoutConfig {
         @Injected() private var nightscoutManager: NightscoutManager!
         @Injected() private var glucoseStorage: GlucoseStorage!
         @Injected() private var healthKitManager: HealthKitManager!
+        @Injected() private var cgmManager: FetchGlucoseManager!
 
         @Published var url = ""
         @Published var secret = ""
         @Published var message = ""
         @Published var connecting = false
         @Published var backfilling = false
-        @Published var isUploadEnabled = false
-
+        @Published var isUploadEnabled = false // Allow uploads
+        @Published var uploadStats = false // Upload Statistics
+        @Published var uploadGlucose = true // Upload Glucose
         @Published var useLocalSource = false
         @Published var localPort: Decimal = 0
 
@@ -26,6 +30,18 @@ extension NightscoutConfig {
             subscribeSetting(\.isUploadEnabled, on: $isUploadEnabled) { isUploadEnabled = $0 }
             subscribeSetting(\.useLocalGlucoseSource, on: $useLocalSource) { useLocalSource = $0 }
             subscribeSetting(\.localGlucosePort, on: $localPort.map(Int.init)) { localPort = Decimal($0) }
+            subscribeSetting(\.uploadStats, on: $uploadStats) { uploadStats = $0 }
+            subscribeSetting(\.uploadGlucose, on: $uploadGlucose, initial: { uploadGlucose = $0 }, didSet: { val in
+                if let cgmManagerG5 = self.cgmManager.glucoseSource.cgmManager as? G5CGMManager {
+                    cgmManagerG5.shouldSyncToRemoteService = val
+                }
+                if let cgmManagerG6 = self.cgmManager.glucoseSource.cgmManager as? G6CGMManager {
+                    cgmManagerG6.shouldSyncToRemoteService = val
+                }
+                if let cgmManagerG7 = self.cgmManager.glucoseSource.cgmManager as? G7CGMManager {
+                    cgmManagerG7.uploadReadings = val
+                }
+            })
         }
 
         func connect() {

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

@@ -44,7 +44,13 @@ extension NightscoutConfig {
                 }
 
                 Section {
-                    Toggle("Allow uploads", isOn: $state.isUploadEnabled)
+                    Toggle("Upload", isOn: $state.isUploadEnabled)
+                    if state.isUploadEnabled {
+                        Toggle("Statistics", isOn: $state.uploadStats)
+                        Toggle("Glucose", isOn: $state.uploadGlucose)
+                    }
+                } header: {
+                    Text("Allow Uploads")
                 }
 
                 Section(header: Text("Local glucose source")) {

+ 5 - 4
FreeAPS/Sources/Modules/OverrideProfilesConfig/OverrideProfilesStateModel.swift

@@ -90,10 +90,11 @@ extension OverrideProfilesConfig {
                 saveOverride.id = id
                 saveOverride.date = Date()
                 if override_target {
-                    if units == .mmolL {
-                        target = target.asMgdL
-                    }
-                    saveOverride.target = target as NSDecimalNumber
+                    saveOverride.target = (
+                        units == .mmolL
+                            ? target.asMgdL
+                            : target
+                    ) as NSDecimalNumber
                 } else { saveOverride.target = 0 }
 
                 if advancedSettings {

+ 1 - 1
FreeAPS/Sources/Modules/PreferencesEditor/PreferencesEditorStateModel.swift

@@ -41,7 +41,7 @@ extension PreferencesEditor {
                     displayName: NSLocalizedString("Max COB", comment: "Max COB"),
                     type: .decimal(keypath: \.maxCOB),
                     infoText: NSLocalizedString(
-                        "This defaults maxCOB to 120 because that’s the most a typical body can absorb over 4 hours. (If someone enters more carbs or stacks more; OpenAPS will just truncate dosing based on 120. Essentially, this just limits AMA as a safety cap against weird COB calculations due to fluky data.)",
+                        "The default of maxCOB is 120. (If someone enters more carbs in one or multiple entries, iAPS will cap COB to maxCOB and keep it at maxCOB until the carbs entered above maxCOB have shown to be absorbed. Essentially, this just limits UAM as a safety cap against weird COB calculations due to fluky data.)",
                         comment: "Max COB"
                     ),
                     settable: self

+ 1 - 1
FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift

@@ -41,7 +41,7 @@ extension Settings {
                     Text("Basal Profile").navigationLink(to: .basalProfileEditor, from: self)
                     Text("Insulin Sensitivities").navigationLink(to: .isfEditor, from: self)
                     Text("Carb Ratios").navigationLink(to: .crEditor, from: self)
-                    Text("Target Ranges").navigationLink(to: .targetsEditor, from: self)
+                    Text("Target Glucose").navigationLink(to: .targetsEditor, from: self)
                     Text("Autotune").navigationLink(to: .autotuneConfig, from: self)
                 }
 

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

@@ -5,7 +5,6 @@ extension StatConfig {
         @Published var overrideHbA1cUnit = false
         @Published var low: Decimal = 4 / 0.0555
         @Published var high: Decimal = 10 / 0.0555
-        @Published var uploadStats = false
         @Published var hours: Decimal = 6
         @Published var xGridLines = false
         @Published var yGridLines: Bool = false
@@ -19,7 +18,6 @@ extension StatConfig {
             self.units = units
 
             subscribeSetting(\.overrideHbA1cUnit, on: $overrideHbA1cUnit) { overrideHbA1cUnit = $0 }
-            subscribeSetting(\.uploadStats, on: $uploadStats) { uploadStats = $0 }
             subscribeSetting(\.xGridLines, on: $xGridLines) { xGridLines = $0 }
             subscribeSetting(\.yGridLines, on: $yGridLines) { yGridLines = $0 }
             subscribeSetting(\.rulerMarks, on: $rulerMarks) { rulerMarks = $0 }

+ 0 - 1
FreeAPS/Sources/Modules/StatConfig/View/StatConfigRootView.swift

@@ -28,7 +28,6 @@ extension StatConfig {
             Form {
                 Section(header: Text("Settings")) {
                     Toggle("Change HbA1c Unit", isOn: $state.overrideHbA1cUnit)
-                    Toggle("Allow Upload of Statistics to NS", isOn: $state.uploadStats)
                     Toggle("Display Chart X - Grid lines", isOn: $state.xGridLines)
                     Toggle("Display Chart Y - Grid lines", isOn: $state.yGridLines)
                     Toggle("Display Chart Threshold lines for Low and High", isOn: $state.rulerMarks)

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

@@ -39,7 +39,7 @@ extension TargetsEditor {
                 }
             }
             .onAppear(perform: configureView)
-            .navigationTitle("Target Ranges")
+            .navigationTitle("Target Glucose")
             .navigationBarTitleDisplayMode(.automatic)
             .navigationBarItems(
                 trailing: EditButton()

+ 3 - 0
FreeAPS/Sources/Modules/WatchConfig/View/WatchConfigRootView.swift

@@ -18,6 +18,9 @@ extension WatchConfig {
                         }
                     }
                 }
+
+                Toggle("Display Protein & Fat", isOn: $state.displayFatAndProteinOnWatch)
+
                 Section(header: Text("Garmin Watch")) {
                     List {
                         ForEach(state.devices, id: \.uuid) { device in

+ 2 - 0
FreeAPS/Sources/Modules/WatchConfig/WatchConfigStateModel.swift

@@ -30,12 +30,14 @@ extension WatchConfig {
         @Injected() private var garmin: GarminManager!
         @Published var devices: [IQDevice] = []
         @Published var selectedAwConfig: AwConfig = .HR
+        @Published var displayFatAndProteinOnWatch = false
 
         private(set) var preferences = Preferences()
 
         override func subscribe() {
             preferences = provider.preferences
 
+            subscribeSetting(\.displayFatAndProteinOnWatch, on: $displayFatAndProteinOnWatch) { displayFatAndProteinOnWatch = $0 }
             subscribeSetting(\.displayOnWatch, on: $selectedAwConfig) { selectedAwConfig = $0 }
             didSet: { [weak self] value in
                 // for compatibility with old displayHR

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

@@ -392,6 +392,8 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                 insulin: nil,
                 notes: nil,
                 carbs: nil,
+                fat: nil,
+                protein: nil,
                 targetTop: nil,
                 targetBottom: nil
             )

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

@@ -78,8 +78,10 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
             var insulinRequired = self.suggestion?.insulinReq ?? 0
             var double: Decimal = 2
             if (self.suggestion?.cob ?? 0) > 0 {
-                insulinRequired = self.suggestion?.insulinForManualBolus ?? 0
-                double = 1
+                if self.suggestion?.manualBolusErrorString == 0 {
+                    insulinRequired = self.suggestion?.insulinForManualBolus ?? 0
+                    double = 1
+                }
             }
 
             self.state.bolusRecommended = self.apsManager
@@ -102,8 +104,8 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
                     )
                 }
             self.state.bolusAfterCarbs = !self.settingsManager.settings.skipBolusScreenAfterCarbs
-
             self.state.displayOnWatch = self.settingsManager.settings.displayOnWatch
+            self.state.displayFatAndProteinOnWatch = self.settingsManager.settings.displayFatAndProteinOnWatch
 
             let eBG = self.evetualBGStraing()
             self.state.eventualBG = eBG.map { "⇢ " + $0 }
@@ -263,16 +265,22 @@ extension BaseWatchManager: WCSessionDelegate {
     func session(_: WCSession, didReceiveMessage message: [String: Any], replyHandler: @escaping ([String: Any]) -> Void) {
         debug(.service, "WCSession got message with reply handler: \(message)")
 
-        if let carbs = message["carbs"] as? Double, carbs > 0 {
-            carbsStorage.storeCarbs([
-                CarbsEntry(
+        if let carbs = message["carbs"] as? Double,
+           let fat = message["fat"] as? Double,
+           let protein = message["protein"] as? Double,
+           carbs > 0 || fat > 0 || protein > 0
+        {
+            carbsStorage.storeCarbs(
+                [CarbsEntry(
                     id: UUID().uuidString,
                     createdAt: Date(),
                     carbs: Decimal(carbs),
+                    fat: Decimal(fat),
+                    protein: Decimal(protein),
                     enteredBy: CarbsEntry.manual,
                     isFPU: false, fpuID: nil
-                )
-            ])
+                )]
+            )
 
             if settingsManager.settings.skipBolusScreenAfterCarbs {
                 apsManager.determineBasalSync()

+ 1 - 0
FreeAPSWatch WatchKit Extension/DataFlow.swift

@@ -21,6 +21,7 @@ struct WatchState: Codable {
     var eventualBG: String?
     var eventualBGRaw: String?
     var displayOnWatch: AwConfig?
+    var displayFatAndProteinOnWatch: Bool?
     var isf: Decimal?
     var override: String?
 }

+ 208 - 45
FreeAPSWatch WatchKit Extension/Views/CarbsView.swift

@@ -3,7 +3,19 @@ import SwiftUI
 struct CarbsView: View {
     @EnvironmentObject var state: WatchStateModel
 
-    @State var amount = 0.0
+    // Selected nutrient
+    enum Selection: String {
+        case carbs
+        case protein
+        case fat
+    }
+
+    @State var selection: Selection = .carbs
+    @State var carbAmount = 0.0
+    @State var fatAmount = 0.0
+    @State var proteinAmount = 0.0
+    @State var colorOfselection: Color = .darkGray
+    // @State var displayPresets: Bool = false
 
     var numberFormatter: NumberFormatter {
         let formatter = NumberFormatter()
@@ -16,62 +28,213 @@ struct CarbsView: View {
     }
 
     var body: some View {
-        GeometryReader { geo in
-            VStack(spacing: 16) {
-                HStack {
-                    Button {
-                        WKInterfaceDevice.current().play(.click)
-                        let newValue = amount - 5
-                        amount = max(newValue, 0)
-                    } label: {
+        VStack {
+            // nutrient
+            carbs
+            if state.displayFatAndProteinOnWatch {
+                Spacer()
+                protein
+                Spacer()
+                fat
+            }
+            buttonStack
+        }
+        .onAppear { carbAmount = Double(state.carbsRequired ?? 0) }
+    }
+
+    var nutrient: some View {
+        HStack {
+            switch selection {
+            case .protein:
+                Text("Protein")
+            case .fat:
+                Text("Fat")
+            default:
+                Text("Carbs")
+            }
+        }.font(.footnote).frame(maxWidth: .infinity, alignment: .center)
+    }
+
+    var carbs: some View {
+        HStack {
+            if selection == .carbs {
+                Button {
+                    WKInterfaceDevice.current().play(.click)
+                    let newValue = carbAmount - 5
+                    carbAmount = max(newValue, 0)
+                }
+                label: {
+                    HStack {
                         Image(systemName: "minus")
+                        Text("") // Ugly fix to increase active tapping (button) area.
                     }
-                    .frame(width: geo.size.width / 4)
-                    Spacer()
-                    Text(numberFormatter.string(from: amount as NSNumber)! + " g")
-                        .font(.title2)
-                        .focusable(true)
-                        .digitalCrownRotation(
-                            $amount,
-                            from: 0,
-                            through: Double(state.maxCOB ?? 120),
-                            by: 1,
-                            sensitivity: .medium,
-                            isContinuous: false,
-                            isHapticFeedbackEnabled: true
-                        )
-                    Spacer()
-                    Button {
-                        WKInterfaceDevice.current().play(.click)
-                        let newValue = amount + 5
-                        amount = min(newValue, Double(state.maxCOB ?? 120))
-                    } label: { Image(systemName: "plus") }
-                        .frame(width: geo.size.width / 4)
                 }
+                .buttonStyle(.borderless).padding(.leading, 5)
+                .tint(selection == .carbs ? .blue : .none)
+            }
+            Spacer()
+            Text("🥨")
+            Spacer()
+            Text(numberFormatter.string(from: carbAmount as NSNumber)! + " g")
+                .font(selection == .carbs ? .title : .title3)
+                .focusable(selection == .carbs)
+                .digitalCrownRotation(
+                    $carbAmount,
+                    from: 0,
+                    through: Double(state.maxCOB ?? 120),
+                    by: 1,
+                    sensitivity: .medium,
+                    isContinuous: false,
+                    isHapticFeedbackEnabled: true
+                )
+            Spacer()
+            if selection == .carbs {
                 Button {
                     WKInterfaceDevice.current().play(.click)
-                    // Get amount from displayed string
-                    let amount = Int(numberFormatter.string(from: amount as NSNumber)!) ?? Int(amount.rounded())
-                    state.addCarbs(amount)
+                    let newValue = carbAmount + 5
+                    carbAmount = min(newValue, Double(state.maxCOB ?? 120))
+                } label: { Image(systemName: "plus") }
+                    .buttonStyle(.borderless).padding(.trailing, 5)
+                    .tint(selection == .carbs ? .blue : .none)
+            }
+        }
+        .minimumScaleFactor(0.7)
+        .onTapGesture {
+            select(entry: .carbs)
+        }
+        .background(selection == .carbs && state.displayFatAndProteinOnWatch ? colorOfselection : .black)
+        .padding(.top)
+    }
+
+    var protein: some View {
+        HStack {
+            if selection == .protein {
+                Button {
+                    WKInterfaceDevice.current().play(.click)
+                    let newValue = proteinAmount - 5
+                    proteinAmount = max(newValue, 0)
+                } label: {
+                    HStack {
+                        Image(systemName: "minus")
+                        Text("") // Ugly fix to increase active tapping (button) area.
+                    }
                 }
-                label: {
+                .buttonStyle(.borderless).padding(.leading, 5)
+                .tint(selection == .protein ? .blue : .none)
+            }
+            Spacer()
+            Text("🍗")
+            Spacer()
+            Text(numberFormatter.string(from: proteinAmount as NSNumber)! + " g")
+                .font(selection == .protein ? .title : .title3)
+                .foregroundStyle(.red)
+                .focusable(selection == .protein)
+                .digitalCrownRotation(
+                    $proteinAmount,
+                    from: 0,
+                    through: Double(240),
+                    by: 1,
+                    sensitivity: .medium,
+                    isContinuous: false,
+                    isHapticFeedbackEnabled: true
+                )
+            Spacer()
+            if selection == .protein {
+                Button {
+                    WKInterfaceDevice.current().play(.click)
+                    let newValue = proteinAmount + 5
+                    proteinAmount = min(newValue, Double(240))
+                } label: { Image(systemName: "plus") }.buttonStyle(.borderless).padding(.trailing, 5)
+                    .tint(selection == .protein ? .blue : .none)
+            }
+        }
+        .minimumScaleFactor(0.7)
+        .onTapGesture {
+            select(entry: .protein)
+        }
+        .background(selection == .protein ? colorOfselection : .black)
+    }
+
+    var fat: some View {
+        HStack {
+            if selection == .fat {
+                Button {
+                    WKInterfaceDevice.current().play(.click)
+                    let newValue = fatAmount - 5
+                    fatAmount = max(newValue, 0)
+                } label: {
                     HStack {
-                        Image("carbs", bundle: nil)
-                            .renderingMode(.template)
-                            .resizable()
-                            .frame(width: 24, height: 24)
-                            .foregroundColor(.loopYellow)
-                        Text("Add Carbs ")
+                        Image(systemName: "minus")
+                        Text("") // Ugly fix to increase active tapping (button) area.
                     }
                 }
-                .disabled(amount <= 0)
-            }.frame(maxHeight: .infinity)
+                .buttonStyle(.borderless).padding(.leading, 5)
+                .tint(selection == .fat ? .blue : .none)
+            }
+            Spacer()
+            Text("🧀")
+            Spacer()
+            Text(numberFormatter.string(from: fatAmount as NSNumber)! + " g")
+                .font(selection == .fat ? .title : .title3)
+                .foregroundColor(.loopYellow)
+                .focusable(selection == .fat)
+                .digitalCrownRotation(
+                    $fatAmount,
+                    from: 0,
+                    through: Double(240),
+                    by: 1,
+                    sensitivity: .medium,
+                    isContinuous: false,
+                    isHapticFeedbackEnabled: true
+                )
+            Spacer()
+            if selection == .fat {
+                Button {
+                    WKInterfaceDevice.current().play(.click)
+                    let newValue = fatAmount + 5
+                    fatAmount = min(newValue, Double(240))
+                } label: { Image(systemName: "plus") }
+                    .buttonStyle(.borderless).padding(.trailing, 5)
+                    .tint(selection == .fat ? .blue : .none)
+            }
         }
-        .navigationTitle("Add Carbs ")
+        .minimumScaleFactor(0.7)
+        .onTapGesture {
+            select(entry: .fat)
+        }
+        .background(selection == .fat ? colorOfselection : .black)
+    }
 
-        .onAppear {
-            amount = Double(state.carbsRequired ?? 0)
+    var buttonStack: some View {
+        HStack(spacing: 25) {
+            /* To do: display the actual meal presets
+             Button {
+                 displayPresets.toggle()
+             }
+             label: { Image(systemName: "menucard.fill") }
+                 .buttonStyle(.borderless)
+             */
+            Button {
+                WKInterfaceDevice.current().play(.click)
+                // Get amount from displayed string
+                let amountCarbs = Int(numberFormatter.string(from: carbAmount as NSNumber)!) ?? Int(carbAmount.rounded())
+                let amountFat = Int(numberFormatter.string(from: fatAmount as NSNumber)!) ?? Int(fatAmount.rounded())
+                let amountProtein = Int(numberFormatter.string(from: proteinAmount as NSNumber)!) ??
+                    Int(proteinAmount.rounded())
+                state.addMeal(amountCarbs, fat: amountFat, protein: amountProtein)
+            }
+            label: { Text("Save") }
+                .buttonStyle(.borderless)
+                .font(.callout)
+                .foregroundColor(carbAmount > 0 || fatAmount > 0 || proteinAmount > 0 ? .blue : .secondary)
+                .disabled(carbAmount <= 0 && fatAmount <= 0 && proteinAmount <= 0)
         }
+        .frame(maxHeight: .infinity, alignment: .bottom)
+        .padding(.top)
+    }
+
+    private func select(entry: Selection) {
+        selection = entry
     }
 }
 

+ 4 - 3
FreeAPSWatch WatchKit Extension/WatchStateModel.swift

@@ -33,6 +33,7 @@ class WatchStateModel: NSObject, ObservableObject {
     @Published var isTempTargetViewActive = false
     @Published var isBolusViewActive = false
     @Published var displayOnWatch: AwConfig = .BGTarget
+    @Published var displayFatAndProteinOnWatch = false
     @Published var eventualBG = ""
     @Published var isConfirmationViewActive = false {
         didSet {
@@ -53,7 +54,6 @@ class WatchStateModel: NSObject, ObservableObject {
     @Published var lastUpdate: Date = .distantPast
     @Published var timerDate = Date()
     @Published var pendingBolus: Double?
-
     @Published var isf: Decimal?
     @Published var override: String?
 
@@ -69,11 +69,11 @@ class WatchStateModel: NSObject, ObservableObject {
         session.activate()
     }
 
-    func addCarbs(_ carbs: Int) {
+    func addMeal(_ carbs: Int, fat: Int, protein: Int) {
         confirmationSuccess = nil
         isConfirmationViewActive = true
         isCarbsViewActive = false
-        session.sendMessage(["carbs": carbs], replyHandler: { reply in
+        session.sendMessage(["carbs": carbs, "fat": fat, "protein": protein], replyHandler: { reply in
             self.completionHandler(reply)
             if let ok = reply["confirmation"] as? Bool, ok, self.bolusAfterCarbs {
                 DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
@@ -173,6 +173,7 @@ class WatchStateModel: NSObject, ObservableObject {
         lastUpdate = Date()
         eventualBG = state.eventualBG ?? ""
         displayOnWatch = state.displayOnWatch ?? .BGTarget
+        displayFatAndProteinOnWatch = state.displayFatAndProteinOnWatch ?? false
         isf = state.isf
         override = state.override
     }

+ 1 - 1
README.md

@@ -56,7 +56,7 @@ iAPS app runs on iPhone or iPod. An iPhone 8 or newer is required.
 
 # Documentation
 
-[Discord iAPS - Server ](https://discord.gg/pmQjr9RDC)
+[Discord iAPS - Server ](https://discord.gg/3eGsdykA6)
 
 [iAPS documentation (under development)](https://iaps.readthedocs.io/en/latest/)