فهرست منبع

Merge branch 'dev'

Conflicts:
	FreeAPS/Sources/Localizations/Main/uk.lproj/Localizable.strings
Jon Mårtensson 2 سال پیش
والد
کامیت
44fc78d4fa
61فایلهای تغییر یافته به همراه739 افزوده شده و 622 حذف شده
  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 */; };
 		190EBCCB29FF13CB00BA767D /* StatConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 190EBCCA29FF13CB00BA767D /* StatConfigRootView.swift */; };
 		1927C8E62744606D00347C69 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1927C8E82744606D00347C69 /* InfoPlist.strings */; };
 		1927C8E62744606D00347C69 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1927C8E82744606D00347C69 /* InfoPlist.strings */; };
 		1935364028496F7D001E0B16 /* Oref2_variables.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1935363F28496F7D001E0B16 /* Oref2_variables.swift */; };
 		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 */; };
 		1967DFBE29D052C200759F30 /* Icons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1967DFBD29D052C200759F30 /* Icons.swift */; };
 		1967DFC029D053AC00759F30 /* IconSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1967DFBF29D053AC00759F30 /* IconSelection.swift */; };
 		1967DFC029D053AC00759F30 /* IconSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1967DFBF29D053AC00759F30 /* IconSelection.swift */; };
 		1967DFC229D053D300759F30 /* IconImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1967DFC129D053D300759F30 /* IconImage.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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		1967DFC129D053D300759F30 /* IconImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconImage.swift; sourceTree = "<group>"; };
@@ -1600,6 +1602,7 @@
 				1967DFBD29D052C200759F30 /* Icons.swift */,
 				1967DFBD29D052C200759F30 /* Icons.swift */,
 				19D4E4EA29FC6A9F00351451 /* TIRforChart.swift */,
 				19D4E4EA29FC6A9F00351451 /* TIRforChart.swift */,
 				19A910352A24D6D700C8951B /* DateFilter.swift */,
 				19A910352A24D6D700C8951B /* DateFilter.swift */,
+				193F6CDC2A512C8F001240FD /* Loops.swift */,
 			);
 			);
 			path = Models;
 			path = Models;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -2631,6 +2634,7 @@
 				38F37828261260DC009DB701 /* Color+Extensions.swift in Sources */,
 				38F37828261260DC009DB701 /* Color+Extensions.swift in Sources */,
 				3811DE3F25C9D4A100A708ED /* SettingsStateModel.swift in Sources */,
 				3811DE3F25C9D4A100A708ED /* SettingsStateModel.swift in Sources */,
 				CE7CA3582A064E2F004BE681 /* ListStateView.swift in Sources */,
 				CE7CA3582A064E2F004BE681 /* ListStateView.swift in Sources */,
+				193F6CDD2A512C8F001240FD /* Loops.swift in Sources */,
 				38B4F3CB25E502E200E76A18 /* WeakObjectSet.swift in Sources */,
 				38B4F3CB25E502E200E76A18 /* WeakObjectSet.swift in Sources */,
 				38E989DD25F5021400C0CED0 /* PumpStatus.swift in Sources */,
 				38E989DD25F5021400C0CED0 /* PumpStatus.swift in Sources */,
 				38E98A2525F52C9300C0CED0 /* IssueReporter.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
+  }
+}

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 1
FreeAPS/Resources/javascript/bundle/autotune-core.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 1
FreeAPS/Resources/javascript/bundle/determine-basal.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 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) {
 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" }');
             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");
             return console.error("Error: carb_ratios " + profile_data.carb_ratio + ' and ' + pumpprofile_data.carb_ratio + " out of bounds");
         } else {
         } 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
 //для 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) {
 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"};
         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
         return rounded
     }
     }
 
 
-    private func medianCalculation(array: [Double]) -> Double {
+    private func medianCalculationDouble(array: [Double]) -> Double {
         guard !array.isEmpty else {
         guard !array.isEmpty else {
             return 0
             return 0
         }
         }
@@ -745,7 +745,138 @@ final class BaseAPSManager: APSManager, Injectable {
         return sorted[length / 2]
         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() {
     private func statistics() {
         let now = Date()
         let now = Date()
         if settingsManager.settings.uploadStats {
         if settingsManager.settings.uploadStats {
@@ -766,22 +897,21 @@ final class BaseAPSManager: APSManager, Injectable {
                 let units = self.settingsManager.settings.units
                 let units = self.settingsManager.settings.units
                 let preferences = settingsManager.preferences
                 let preferences = settingsManager.preferences
 
 
+                // Carbs
                 var carbs = [Carbohydrates]()
                 var carbs = [Carbohydrates]()
                 var carbTotal: Decimal = 0
                 var carbTotal: Decimal = 0
                 let requestCarbs = Carbohydrates.fetchRequest() as NSFetchRequest<Carbohydrates>
                 let requestCarbs = Carbohydrates.fetchRequest() as NSFetchRequest<Carbohydrates>
                 let daysAgo = Date().addingTimeInterval(-1.days.timeInterval)
                 let daysAgo = Date().addingTimeInterval(-1.days.timeInterval)
                 requestCarbs.predicate = NSPredicate(format: "carbs > 0 AND date > %@", daysAgo as NSDate)
                 requestCarbs.predicate = NSPredicate(format: "carbs > 0 AND date > %@", daysAgo as NSDate)
-
                 let sortCarbs = NSSortDescriptor(key: "date", ascending: true)
                 let sortCarbs = NSSortDescriptor(key: "date", ascending: true)
                 requestCarbs.sortDescriptors = [sortCarbs]
                 requestCarbs.sortDescriptors = [sortCarbs]
                 try? carbs = coredataContext.fetch(requestCarbs)
                 try? carbs = coredataContext.fetch(requestCarbs)
-
                 carbTotal = carbs.map({ carbs in carbs.carbs as? Decimal ?? 0 }).reduce(0, +)
                 carbTotal = carbs.map({ carbs in carbs.carbs as? Decimal ?? 0 }).reduce(0, +)
 
 
+                // TDD
                 var tdds = [TDD]()
                 var tdds = [TDD]()
                 var currentTDD: Decimal = 0
                 var currentTDD: Decimal = 0
                 var tddTotalAverage: Decimal = 0
                 var tddTotalAverage: Decimal = 0
-
                 let requestTDD = TDD.fetchRequest() as NSFetchRequest<TDD>
                 let requestTDD = TDD.fetchRequest() as NSFetchRequest<TDD>
                 let sort = NSSortDescriptor(key: "timestamp", ascending: false)
                 let sort = NSSortDescriptor(key: "timestamp", ascending: false)
                 let daysOf14Ago = Date().addingTimeInterval(-14.days.timeInterval)
                 let daysOf14Ago = Date().addingTimeInterval(-14.days.timeInterval)
@@ -806,7 +936,6 @@ final class BaseAPSManager: APSManager, Injectable {
                 } else if preferences.useNewFormula, !preferences.sigmoid,!preferences.enableDynamicCR {
                 } else if preferences.useNewFormula, !preferences.sigmoid,!preferences.enableDynamicCR {
                     algo_ = "Dynamic ISF: Logarithmic"
                     algo_ = "Dynamic ISF: Logarithmic"
                 }
                 }
-
                 let af = preferences.adjustmentFactor
                 let af = preferences.adjustmentFactor
                 let insulin_type = preferences.curve
                 let insulin_type = preferences.curve
                 let buildDate = Bundle.main.buildDate
                 let buildDate = Bundle.main.buildDate
@@ -825,284 +954,79 @@ final class BaseAPSManager: APSManager, Injectable {
                 } else if preferences.curve.rawValue == "ultra-rapid" {
                 } else if preferences.curve.rawValue == "ultra-rapid" {
                     iPa = 50
                     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 requestGFS = Readings.fetchRequest() as NSFetchRequest<Readings>
                 let sortGlucose = NSSortDescriptor(key: "date", ascending: false)
                 let sortGlucose = NSSortDescriptor(key: "date", ascending: false)
+                requestGFS.predicate = NSPredicate(format: "glucose > 0 AND date > %@", filter.total)
                 requestGFS.sortDescriptors = [sortGlucose]
                 requestGFS.sortDescriptors = [sortGlucose]
-
                 try? glucose = coredataContext.fetch(requestGFS)
                 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(
                 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
                 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(
                 let tir = Durations(
                     day: roundDecimal(Decimal(oneDay_.TIR), 1),
                     day: roundDecimal(Decimal(oneDay_.TIR), 1),
@@ -1110,53 +1034,89 @@ final class BaseAPSManager: APSManager, Injectable {
                     month: roundDecimal(Decimal(thirtyDays_.TIR), 1),
                     month: roundDecimal(Decimal(thirtyDays_.TIR), 1),
                     total: roundDecimal(Decimal(totalDays_.TIR), 1)
                     total: roundDecimal(Decimal(totalDays_.TIR), 1)
                 )
                 )
-
                 let hypo = Durations(
                 let hypo = Durations(
                     day: Decimal(oneDay_.hypos),
                     day: Decimal(oneDay_.hypos),
                     week: Decimal(sevenDays_.hypos),
                     week: Decimal(sevenDays_.hypos),
                     month: Decimal(thirtyDays_.hypos),
                     month: Decimal(thirtyDays_.hypos),
                     total: Decimal(totalDays_.hypos)
                     total: Decimal(totalDays_.hypos)
                 )
                 )
-
                 let hyper = Durations(
                 let hyper = Durations(
                     day: Decimal(oneDay_.hypers),
                     day: Decimal(oneDay_.hypers),
                     week: Decimal(sevenDays_.hypers),
                     week: Decimal(sevenDays_.hypers),
                     month: Decimal(thirtyDays_.hypers),
                     month: Decimal(thirtyDays_.hypers),
                     total: Decimal(totalDays_.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(
                 let range = Threshold(
                     low: units == .mmolL ? roundDecimal(settingsManager.settings.low.asMmolL, 1) :
                     low: units == .mmolL ? roundDecimal(settingsManager.settings.low.asMmolL, 1) :
                         roundDecimal(settingsManager.settings.low, 0),
                         roundDecimal(settingsManager.settings.low, 0),
                     high: units == .mmolL ? roundDecimal(settingsManager.settings.high.asMmolL, 1) :
                     high: units == .mmolL ? roundDecimal(settingsManager.settings.high.asMmolL, 1) :
                         roundDecimal(settingsManager.settings.high, 0)
                         roundDecimal(settingsManager.settings.high, 0)
                 )
                 )
-
                 let TimeInRange = TIRs(
                 let TimeInRange = TIRs(
                     TIR: tir,
                     TIR: tir,
                     Hypos: hypo,
                     Hypos: hypo,
                     Hypers: hyper,
                     Hypers: hyper,
-                    Threshold: range
+                    Threshold: range,
+                    Euglycemic: normal
                 )
                 )
-
                 let avgs = Durations(
                 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 insulinDistribution = [InsulinDistribution]()
-
                 var insulin = Ins(
                 var insulin = Ins(
                     TDD: 0,
                     TDD: 0,
                     bolus: 0,
                     bolus: 0,
@@ -1164,13 +1124,10 @@ final class BaseAPSManager: APSManager, Injectable {
                     scheduled_basal: 0,
                     scheduled_basal: 0,
                     total_average: 0
                     total_average: 0
                 )
                 )
-
                 let requestInsulinDistribution = InsulinDistribution.fetchRequest() as NSFetchRequest<InsulinDistribution>
                 let requestInsulinDistribution = InsulinDistribution.fetchRequest() as NSFetchRequest<InsulinDistribution>
                 let sortInsulin = NSSortDescriptor(key: "date", ascending: false)
                 let sortInsulin = NSSortDescriptor(key: "date", ascending: false)
                 requestInsulinDistribution.sortDescriptors = [sortInsulin]
                 requestInsulinDistribution.sortDescriptors = [sortInsulin]
-
                 try? insulinDistribution = coredataContext.fetch(requestInsulinDistribution)
                 try? insulinDistribution = coredataContext.fetch(requestInsulinDistribution)
-
                 insulin = Ins(
                 insulin = Ins(
                     TDD: roundDecimal(currentTDD, 2),
                     TDD: roundDecimal(currentTDD, 2),
                     bolus: insulinDistribution.first != nil ? ((insulinDistribution.first?.bolus ?? 0) as Decimal) : 0,
                     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)
                     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(
                 let dailystat = Statistics(
                     created_at: Date(),
                     created_at: Date(),
@@ -1268,13 +1159,12 @@ final class BaseAPSManager: APSManager, Injectable {
                     Statistics: Stats(
                     Statistics: Stats(
                         Distribution: TimeInRange,
                         Distribution: TimeInRange,
                         Glucose: avg,
                         Glucose: avg,
-                        HbA1c: hbs,
+                        HbA1c: hbs, Units: Units(Glucose: units.rawValue, HbA1c: hbA1cUnit),
                         LoopCycles: loopstat,
                         LoopCycles: loopstat,
                         Insulin: insulin,
                         Insulin: insulin,
                         Variance: variance
                         Variance: variance
                     )
                     )
                 )
                 )
-
                 storage.save(dailystat, as: file)
                 storage.save(dailystat, as: file)
                 nightscout.uploadStatistics(dailystat: dailystat)
                 nightscout.uploadStatistics(dailystat: dailystat)
                 nightscout.uploadPreferences()
                 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")
     private let processQueue = DispatchQueue(label: "BaseCarbsStorage.processQueue")
     @Injected() private var storage: FileStorage!
     @Injected() private var storage: FileStorage!
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var broadcaster: Broadcaster!
+    @Injected() private var settings: SettingsManager!
 
 
     let coredataContext = CoreDataStack.shared.persistentContainer.newBackgroundContext()
     let coredataContext = CoreDataStack.shared.persistentContainer.newBackgroundContext()
 
 
@@ -26,25 +27,97 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         injectServices(resolver)
         injectServices(resolver)
     }
     }
 
 
-    func storeCarbs(_ carbs: [CarbsEntry]) {
+    func storeCarbs(_ entries: [CarbsEntry]) {
         processQueue.sync {
         processQueue.sync {
             let file = OpenAPS.Monitor.carbHistory
             let file = OpenAPS.Monitor.carbHistory
             var uniqEvents: [CarbsEntry] = []
             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
             // MARK: Save to CoreData. TEST
 
 
             var cbs: Decimal = 0
             var cbs: Decimal = 0
             var carbDate = Date()
             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 {
             if cbs != 0 {
                 self.coredataContext.perform {
                 self.coredataContext.perform {
@@ -56,7 +129,6 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
                     try? self.coredataContext.save()
                     try? self.coredataContext.save()
                 }
                 }
             }
             }
-
             broadcaster.notify(CarbsObserver.self, on: processQueue) {
             broadcaster.notify(CarbsObserver.self, on: processQueue) {
                 $0.carbsDidUpdate(uniqEvents)
                 $0.carbsDidUpdate(uniqEvents)
             }
             }
@@ -115,6 +187,8 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
                 insulin: nil,
                 insulin: nil,
                 notes: nil,
                 notes: nil,
                 carbs: $0.carbs,
                 carbs: $0.carbs,
+                fat: nil,
+                protein: nil,
                 targetTop: nil,
                 targetTop: nil,
                 targetBottom: nil
                 targetBottom: nil
             )
             )

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

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

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

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

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

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

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 4
FreeAPS/Sources/Localizations/Main/ar.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 3
FreeAPS/Sources/Localizations/Main/ca.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 4
FreeAPS/Sources/Localizations/Main/da.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 4
FreeAPS/Sources/Localizations/Main/de.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 4
FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 4
FreeAPS/Sources/Localizations/Main/es.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 4
FreeAPS/Sources/Localizations/Main/fi.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 4
FreeAPS/Sources/Localizations/Main/fr.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 4
FreeAPS/Sources/Localizations/Main/he.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 1
FreeAPS/Sources/Localizations/Main/it.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 1
FreeAPS/Sources/Localizations/Main/nb.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 4
FreeAPS/Sources/Localizations/Main/nl.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 4
FreeAPS/Sources/Localizations/Main/pl.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 4
FreeAPS/Sources/Localizations/Main/pt-BR.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 4
FreeAPS/Sources/Localizations/Main/pt-PT.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 4
FreeAPS/Sources/Localizations/Main/ru.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 4
FreeAPS/Sources/Localizations/Main/sk.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 4
FreeAPS/Sources/Localizations/Main/sv.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 4
FreeAPS/Sources/Localizations/Main/tr.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 7
FreeAPS/Sources/Localizations/Main/uk.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 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 id: String?
     let createdAt: Date
     let createdAt: Date
     let carbs: Decimal
     let carbs: Decimal
+    let fat: Decimal?
+    let protein: Decimal?
     let enteredBy: String?
     let enteredBy: String?
     let isFPU: Bool?
     let isFPU: Bool?
     let fpuID: String?
     let fpuID: String?
@@ -25,6 +27,8 @@ extension CarbsEntry {
         case id = "_id"
         case id = "_id"
         case createdAt = "created_at"
         case createdAt = "created_at"
         case carbs
         case carbs
+        case fat
+        case protein
         case enteredBy
         case enteredBy
         case isFPU
         case isFPU
         case fpuID
         case fpuID

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

@@ -41,6 +41,7 @@ struct FreeAPSSettings: JSON, Equatable {
     var oneDimensionalGraph: Bool = false
     var oneDimensionalGraph: Bool = false
     var rulerMarks: Bool = false
     var rulerMarks: Bool = false
     var maxCarbs: Decimal = 1000
     var maxCarbs: Decimal = 1000
+    var displayFatAndProteinOnWatch: Bool = false
 }
 }
 
 
 extension FreeAPSSettings: Decodable {
 extension FreeAPSSettings: Decodable {
@@ -214,6 +215,10 @@ extension FreeAPSSettings: Decodable {
             settings.maxCarbs = maxCarbs
             settings.maxCarbs = maxCarbs
         }
         }
 
 
+        if let displayFatAndProteinOnWatch = try? container.decode(Bool.self, forKey: .displayFatAndProteinOnWatch) {
+            settings.displayFatAndProteinOnWatch = displayFatAndProteinOnWatch
+        }
+
         self = settings
         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 insulin: Decimal?
     var notes: String?
     var notes: String?
     var carbs: Decimal?
     var carbs: Decimal?
+    var fat: Decimal?
+    var protein: Decimal?
     let targetTop: Decimal?
     let targetTop: Decimal?
     let targetBottom: Decimal?
     let targetBottom: Decimal?
 
 
@@ -43,6 +45,8 @@ extension NigtscoutTreatment {
         case insulin
         case insulin
         case notes
         case notes
         case carbs
         case carbs
+        case fat
+        case protein
         case targetTop
         case targetTop
         case targetBottom
         case targetBottom
     }
     }

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

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

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

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

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

@@ -33,79 +33,17 @@ extension AddCarbs {
             }
             }
             carbs = min(carbs, maxCarbs)
             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 {
             if settingsManager.settings.skipBolusScreenAfterCarbs {
                 apsManager.determineBasalSync()
                 apsManager.determineBasalSync()

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

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

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

@@ -248,7 +248,7 @@ extension Bolus {
                         "which is below your Threshold (",
                         "which is below your Threshold (",
                         comment: "Bolus pop-up / Alert string. Make translations concise!"
                         comment: "Bolus pop-up / Alert string. Make translations concise!"
                     ) + state
                     ) + state
-                    .threshold.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) + ")"
+                    .threshold.formatted() + " " + state.units.rawValue + ")"
             case 3:
             case 3:
                 return NSLocalizedString(
                 return NSLocalizedString(
                     "Eventual Glucose > Target Glucose, but glucose is climbing slower than expected. Expected: ",
                     "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
             cgmTransmitterDeviceAddress = UserDefaults.standard.cgmTransmitterDeviceAddress
 
 
             subscribeSetting(\.useCalendar, on: $createCalendarEvents) { createCalendarEvents = $0 }
             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 })
             subscribeSetting(\.smoothGlucose, on: $smoothGlucose, initial: { smoothGlucose = $0 })
 
 
             $cgm
             $cgm
@@ -91,7 +79,6 @@ extension CGM.StateModel: CompletionDelegate {
         }
         }
         // refresh the upload options
         // refresh the upload options
         uploadGlucose = settingsManager.settings.uploadGlucose
         uploadGlucose = settingsManager.settings.uploadGlucose
-
         cgmManager.updateGlucoseSource()
         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")) {
                     Section(header: Text("Experimental")) {
                         Toggle("Smooth Glucose Value", isOn: $state.smoothGlucose)
                         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 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 {
         var canAdd: Bool {
             guard let lastItem = items.last else { return true }
             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 Combine
+import G7SensorKit
 import SwiftDate
 import SwiftDate
 import SwiftUI
 import SwiftUI
 
 
@@ -8,14 +10,16 @@ extension NightscoutConfig {
         @Injected() private var nightscoutManager: NightscoutManager!
         @Injected() private var nightscoutManager: NightscoutManager!
         @Injected() private var glucoseStorage: GlucoseStorage!
         @Injected() private var glucoseStorage: GlucoseStorage!
         @Injected() private var healthKitManager: HealthKitManager!
         @Injected() private var healthKitManager: HealthKitManager!
+        @Injected() private var cgmManager: FetchGlucoseManager!
 
 
         @Published var url = ""
         @Published var url = ""
         @Published var secret = ""
         @Published var secret = ""
         @Published var message = ""
         @Published var message = ""
         @Published var connecting = false
         @Published var connecting = false
         @Published var backfilling = 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 useLocalSource = false
         @Published var localPort: Decimal = 0
         @Published var localPort: Decimal = 0
 
 
@@ -26,6 +30,18 @@ extension NightscoutConfig {
             subscribeSetting(\.isUploadEnabled, on: $isUploadEnabled) { isUploadEnabled = $0 }
             subscribeSetting(\.isUploadEnabled, on: $isUploadEnabled) { isUploadEnabled = $0 }
             subscribeSetting(\.useLocalGlucoseSource, on: $useLocalSource) { useLocalSource = $0 }
             subscribeSetting(\.useLocalGlucoseSource, on: $useLocalSource) { useLocalSource = $0 }
             subscribeSetting(\.localGlucosePort, on: $localPort.map(Int.init)) { localPort = Decimal($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() {
         func connect() {

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

@@ -44,7 +44,13 @@ extension NightscoutConfig {
                 }
                 }
 
 
                 Section {
                 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")) {
                 Section(header: Text("Local glucose source")) {

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

@@ -90,10 +90,11 @@ extension OverrideProfilesConfig {
                 saveOverride.id = id
                 saveOverride.id = id
                 saveOverride.date = Date()
                 saveOverride.date = Date()
                 if override_target {
                 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 }
                 } else { saveOverride.target = 0 }
 
 
                 if advancedSettings {
                 if advancedSettings {

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

@@ -41,7 +41,7 @@ extension PreferencesEditor {
                     displayName: NSLocalizedString("Max COB", comment: "Max COB"),
                     displayName: NSLocalizedString("Max COB", comment: "Max COB"),
                     type: .decimal(keypath: \.maxCOB),
                     type: .decimal(keypath: \.maxCOB),
                     infoText: NSLocalizedString(
                     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"
                         comment: "Max COB"
                     ),
                     ),
                     settable: self
                     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("Basal Profile").navigationLink(to: .basalProfileEditor, from: self)
                     Text("Insulin Sensitivities").navigationLink(to: .isfEditor, from: self)
                     Text("Insulin Sensitivities").navigationLink(to: .isfEditor, from: self)
                     Text("Carb Ratios").navigationLink(to: .crEditor, 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)
                     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 overrideHbA1cUnit = false
         @Published var low: Decimal = 4 / 0.0555
         @Published var low: Decimal = 4 / 0.0555
         @Published var high: Decimal = 10 / 0.0555
         @Published var high: Decimal = 10 / 0.0555
-        @Published var uploadStats = false
         @Published var hours: Decimal = 6
         @Published var hours: Decimal = 6
         @Published var xGridLines = false
         @Published var xGridLines = false
         @Published var yGridLines: Bool = false
         @Published var yGridLines: Bool = false
@@ -19,7 +18,6 @@ extension StatConfig {
             self.units = units
             self.units = units
 
 
             subscribeSetting(\.overrideHbA1cUnit, on: $overrideHbA1cUnit) { overrideHbA1cUnit = $0 }
             subscribeSetting(\.overrideHbA1cUnit, on: $overrideHbA1cUnit) { overrideHbA1cUnit = $0 }
-            subscribeSetting(\.uploadStats, on: $uploadStats) { uploadStats = $0 }
             subscribeSetting(\.xGridLines, on: $xGridLines) { xGridLines = $0 }
             subscribeSetting(\.xGridLines, on: $xGridLines) { xGridLines = $0 }
             subscribeSetting(\.yGridLines, on: $yGridLines) { yGridLines = $0 }
             subscribeSetting(\.yGridLines, on: $yGridLines) { yGridLines = $0 }
             subscribeSetting(\.rulerMarks, on: $rulerMarks) { rulerMarks = $0 }
             subscribeSetting(\.rulerMarks, on: $rulerMarks) { rulerMarks = $0 }

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

@@ -28,7 +28,6 @@ extension StatConfig {
             Form {
             Form {
                 Section(header: Text("Settings")) {
                 Section(header: Text("Settings")) {
                     Toggle("Change HbA1c Unit", isOn: $state.overrideHbA1cUnit)
                     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 X - Grid lines", isOn: $state.xGridLines)
                     Toggle("Display Chart Y - Grid lines", isOn: $state.yGridLines)
                     Toggle("Display Chart Y - Grid lines", isOn: $state.yGridLines)
                     Toggle("Display Chart Threshold lines for Low and High", isOn: $state.rulerMarks)
                     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)
             .onAppear(perform: configureView)
-            .navigationTitle("Target Ranges")
+            .navigationTitle("Target Glucose")
             .navigationBarTitleDisplayMode(.automatic)
             .navigationBarTitleDisplayMode(.automatic)
             .navigationBarItems(
             .navigationBarItems(
                 trailing: EditButton()
                 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")) {
                 Section(header: Text("Garmin Watch")) {
                     List {
                     List {
                         ForEach(state.devices, id: \.uuid) { device in
                         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!
         @Injected() private var garmin: GarminManager!
         @Published var devices: [IQDevice] = []
         @Published var devices: [IQDevice] = []
         @Published var selectedAwConfig: AwConfig = .HR
         @Published var selectedAwConfig: AwConfig = .HR
+        @Published var displayFatAndProteinOnWatch = false
 
 
         private(set) var preferences = Preferences()
         private(set) var preferences = Preferences()
 
 
         override func subscribe() {
         override func subscribe() {
             preferences = provider.preferences
             preferences = provider.preferences
 
 
+            subscribeSetting(\.displayFatAndProteinOnWatch, on: $displayFatAndProteinOnWatch) { displayFatAndProteinOnWatch = $0 }
             subscribeSetting(\.displayOnWatch, on: $selectedAwConfig) { selectedAwConfig = $0 }
             subscribeSetting(\.displayOnWatch, on: $selectedAwConfig) { selectedAwConfig = $0 }
             didSet: { [weak self] value in
             didSet: { [weak self] value in
                 // for compatibility with old displayHR
                 // for compatibility with old displayHR

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

@@ -392,6 +392,8 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                 insulin: nil,
                 insulin: nil,
                 notes: nil,
                 notes: nil,
                 carbs: nil,
                 carbs: nil,
+                fat: nil,
+                protein: nil,
                 targetTop: nil,
                 targetTop: nil,
                 targetBottom: 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 insulinRequired = self.suggestion?.insulinReq ?? 0
             var double: Decimal = 2
             var double: Decimal = 2
             if (self.suggestion?.cob ?? 0) > 0 {
             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
             self.state.bolusRecommended = self.apsManager
@@ -102,8 +104,8 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
                     )
                     )
                 }
                 }
             self.state.bolusAfterCarbs = !self.settingsManager.settings.skipBolusScreenAfterCarbs
             self.state.bolusAfterCarbs = !self.settingsManager.settings.skipBolusScreenAfterCarbs
-
             self.state.displayOnWatch = self.settingsManager.settings.displayOnWatch
             self.state.displayOnWatch = self.settingsManager.settings.displayOnWatch
+            self.state.displayFatAndProteinOnWatch = self.settingsManager.settings.displayFatAndProteinOnWatch
 
 
             let eBG = self.evetualBGStraing()
             let eBG = self.evetualBGStraing()
             self.state.eventualBG = eBG.map { "⇢ " + $0 }
             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) {
     func session(_: WCSession, didReceiveMessage message: [String: Any], replyHandler: @escaping ([String: Any]) -> Void) {
         debug(.service, "WCSession got message with reply handler: \(message)")
         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,
                     id: UUID().uuidString,
                     createdAt: Date(),
                     createdAt: Date(),
                     carbs: Decimal(carbs),
                     carbs: Decimal(carbs),
+                    fat: Decimal(fat),
+                    protein: Decimal(protein),
                     enteredBy: CarbsEntry.manual,
                     enteredBy: CarbsEntry.manual,
                     isFPU: false, fpuID: nil
                     isFPU: false, fpuID: nil
-                )
-            ])
+                )]
+            )
 
 
             if settingsManager.settings.skipBolusScreenAfterCarbs {
             if settingsManager.settings.skipBolusScreenAfterCarbs {
                 apsManager.determineBasalSync()
                 apsManager.determineBasalSync()

+ 1 - 0
FreeAPSWatch WatchKit Extension/DataFlow.swift

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

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

@@ -3,7 +3,19 @@ import SwiftUI
 struct CarbsView: View {
 struct CarbsView: View {
     @EnvironmentObject var state: WatchStateModel
     @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 {
     var numberFormatter: NumberFormatter {
         let formatter = NumberFormatter()
         let formatter = NumberFormatter()
@@ -16,62 +28,213 @@ struct CarbsView: View {
     }
     }
 
 
     var body: some 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")
                         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 {
                 Button {
                     WKInterfaceDevice.current().play(.click)
                     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 {
                     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 isTempTargetViewActive = false
     @Published var isBolusViewActive = false
     @Published var isBolusViewActive = false
     @Published var displayOnWatch: AwConfig = .BGTarget
     @Published var displayOnWatch: AwConfig = .BGTarget
+    @Published var displayFatAndProteinOnWatch = false
     @Published var eventualBG = ""
     @Published var eventualBG = ""
     @Published var isConfirmationViewActive = false {
     @Published var isConfirmationViewActive = false {
         didSet {
         didSet {
@@ -53,7 +54,6 @@ class WatchStateModel: NSObject, ObservableObject {
     @Published var lastUpdate: Date = .distantPast
     @Published var lastUpdate: Date = .distantPast
     @Published var timerDate = Date()
     @Published var timerDate = Date()
     @Published var pendingBolus: Double?
     @Published var pendingBolus: Double?
-
     @Published var isf: Decimal?
     @Published var isf: Decimal?
     @Published var override: String?
     @Published var override: String?
 
 
@@ -69,11 +69,11 @@ class WatchStateModel: NSObject, ObservableObject {
         session.activate()
         session.activate()
     }
     }
 
 
-    func addCarbs(_ carbs: Int) {
+    func addMeal(_ carbs: Int, fat: Int, protein: Int) {
         confirmationSuccess = nil
         confirmationSuccess = nil
         isConfirmationViewActive = true
         isConfirmationViewActive = true
         isCarbsViewActive = false
         isCarbsViewActive = false
-        session.sendMessage(["carbs": carbs], replyHandler: { reply in
+        session.sendMessage(["carbs": carbs, "fat": fat, "protein": protein], replyHandler: { reply in
             self.completionHandler(reply)
             self.completionHandler(reply)
             if let ok = reply["confirmation"] as? Bool, ok, self.bolusAfterCarbs {
             if let ok = reply["confirmation"] as? Bool, ok, self.bolusAfterCarbs {
                 DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                 DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
@@ -173,6 +173,7 @@ class WatchStateModel: NSObject, ObservableObject {
         lastUpdate = Date()
         lastUpdate = Date()
         eventualBG = state.eventualBG ?? ""
         eventualBG = state.eventualBG ?? ""
         displayOnWatch = state.displayOnWatch ?? .BGTarget
         displayOnWatch = state.displayOnWatch ?? .BGTarget
+        displayFatAndProteinOnWatch = state.displayFatAndProteinOnWatch ?? false
         isf = state.isf
         isf = state.isf
         override = state.override
         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
 # 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/)
 [iAPS documentation (under development)](https://iaps.readthedocs.io/en/latest/)