Просмотр исходного кода

Merge branch 'bdb_dev' into Crowdin

Jon B.M 3 лет назад
Родитель
Сommit
c9ca40e218
30 измененных файлов с 506 добавлено и 71 удалено
  1. 1 1
      BGaverages+CoreDataProperties.swift
  2. 1 1
      BGmedian+CoreDataProperties.swift
  3. 1 1
      Carbohydrates+CoreDataProperties.swift
  4. 1 1
      Config.xcconfig
  5. 6 0
      Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents
  6. 4 0
      FreeAPS.xcodeproj/project.pbxproj
  7. 6 0
      FreeAPS.xcworkspace/contents.xcworkspacedata
  8. 1 1
      FreeAPS/Resources/javascript/bundle/determine-basal.js
  9. 29 33
      FreeAPS/Sources/APS/APSManager.swift
  10. 2 2
      FreeAPS/Sources/APS/CGM/GlucoseSimulatorSource.swift
  11. 15 0
      FreeAPS/Sources/APS/FetchGlucoseManager.swift
  12. 6 0
      FreeAPS/Sources/Application/FreeAPSApp.swift
  13. 6 2
      FreeAPS/Sources/Helpers/CoreDataStack.swift
  14. 172 0
      FreeAPS/Sources/Helpers/SavitzkyGolayFilter.swift
  15. 21 0
      FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings
  16. 11 0
      FreeAPS/Sources/Models/BloodGlucose.swift
  17. 5 0
      FreeAPS/Sources/Models/FreeAPSSettings.swift
  18. 19 0
      FreeAPS/Sources/Modules/AddCarbs/AddCarbsStateModel.swift
  19. 83 6
      FreeAPS/Sources/Modules/AddCarbs/View/AddCarbsRootView.swift
  20. 3 0
      FreeAPS/Sources/Modules/CGM/CGMStateModel.swift
  21. 4 0
      FreeAPS/Sources/Modules/CGM/View/CGMRootView.swift
  22. 3 0
      FreeAPS/Sources/Modules/Home/HomeStateModel.swift
  23. 48 0
      FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift
  24. 2 1
      FreeAPS/Sources/Modules/Home/View/HomeRootView.swift
  25. 19 2
      FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift
  26. 2 2
      InsulinDistribution+CoreDataProperties.swift
  27. 1 1
      LoopStatRecord+CoreDataProperties.swift
  28. 17 17
      Oref0Suggestion+CoreDataProperties.swift
  29. 4 0
      Presets+CoreDataClass.swift
  30. 13 0
      Presets+CoreDataProperties.swift

+ 1 - 1
BGaverages+CoreDataProperties.swift

@@ -6,10 +6,10 @@ public extension BGaverages {
         NSFetchRequest<BGaverages>(entityName: "BGaverages")
     }
 
-    @NSManaged var date: Date?
     @NSManaged var average: NSDecimalNumber?
     @NSManaged var average_1: NSDecimalNumber?
     @NSManaged var average_7: NSDecimalNumber?
     @NSManaged var average_30: NSDecimalNumber?
     @NSManaged var average_90: NSDecimalNumber?
+    @NSManaged var date: Date?
 }

+ 1 - 1
BGmedian+CoreDataProperties.swift

@@ -9,7 +9,7 @@ public extension BGmedian {
     @NSManaged var date: Date?
     @NSManaged var median: NSDecimalNumber?
     @NSManaged var median_1: NSDecimalNumber?
+    @NSManaged var median_7: NSDecimalNumber?
     @NSManaged var median_30: NSDecimalNumber?
     @NSManaged var median_90: NSDecimalNumber?
-    @NSManaged var median_7: NSDecimalNumber?
 }

+ 1 - 1
Carbohydrates+CoreDataProperties.swift

@@ -6,7 +6,7 @@ public extension Carbohydrates {
         NSFetchRequest<Carbohydrates>(entityName: "Carbohydrates")
     }
 
-    @NSManaged var date: Date?
     @NSManaged var carbs: NSDecimalNumber?
+    @NSManaged var date: Date?
     @NSManaged var enteredBy: String?
 }

+ 1 - 1
Config.xcconfig

@@ -1,5 +1,5 @@
 APP_DISPLAY_NAME = FreeAPS X
-APP_VERSION = 1.0.9
+APP_VERSION = 1.1.0
 APP_BUILD_NUMBER = 1
 COPYRIGHT_NOTICE =
 DEVELOPER_TEAM = ##TEAM_ID##

+ 6 - 0
Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents

@@ -46,6 +46,12 @@
         <relationship name="computedInsulinDistribution" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="InsulinDistribution" inverseName="insulin" inverseEntity="InsulinDistribution"/>
         <relationship name="computedTDD" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="TDD" inverseName="computed" inverseEntity="TDD"/>
     </entity>
+    <entity name="Presets" representedClassName="Presets" syncable="YES" codeGenerationType="class">
+        <attribute name="carbs" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
+        <attribute name="dish" optional="YES" attributeType="String"/>
+        <attribute name="fat" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
+        <attribute name="protein" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
+    </entity>
     <entity name="Readings" representedClassName="Readings" syncable="YES" codeGenerationType="class">
         <attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
         <attribute name="glucose" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>

+ 4 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -308,6 +308,7 @@
 		CE79502F29980E5800FA576E /* ShareClientUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CE79502D29980E4D00FA576E /* ShareClientUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		CE82E02528E867BA00473A9C /* AlertStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE82E02428E867BA00473A9C /* AlertStorage.swift */; };
 		CE82E02728E869DF00473A9C /* AlertEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE82E02628E869DF00473A9C /* AlertEntry.swift */; };
+		CEA4F62329BE10F70011ADF7 /* SavitzkyGolayFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEA4F62229BE10F70011ADF7 /* SavitzkyGolayFilter.swift */; };
 		CEB434DC28B8F5B900B70274 /* MKRingProgressView.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEB434DB28B8F5B900B70274 /* MKRingProgressView.framework */; };
 		CEB434DD28B8F5B900B70274 /* MKRingProgressView.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CEB434DB28B8F5B900B70274 /* MKRingProgressView.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		CEB434DF28B8F5C400B70274 /* OmniBLE.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEB434DE28B8F5C400B70274 /* OmniBLE.framework */; };
@@ -766,6 +767,7 @@
 		CE79502D29980E4D00FA576E /* ShareClientUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = ShareClientUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		CE82E02428E867BA00473A9C /* AlertStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertStorage.swift; sourceTree = "<group>"; };
 		CE82E02628E869DF00473A9C /* AlertEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertEntry.swift; sourceTree = "<group>"; };
+		CEA4F62229BE10F70011ADF7 /* SavitzkyGolayFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavitzkyGolayFilter.swift; sourceTree = "<group>"; };
 		CEB434DB28B8F5B900B70274 /* MKRingProgressView.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MKRingProgressView.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		CEB434DE28B8F5C400B70274 /* OmniBLE.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OmniBLE.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		CEB434E228B8F9DB00B70274 /* BluetoothStateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothStateManager.swift; sourceTree = "<group>"; };
@@ -1451,6 +1453,7 @@
 				CEB434E428B8FF5D00B70274 /* UIColor.swift */,
 				FE66D16A291F74F8005D6F77 /* Bundle+Extensions.swift */,
 				FEFFA7A12929FE49007B8193 /* UIDevice+Extensions.swift */,
+				CEA4F62229BE10F70011ADF7 /* SavitzkyGolayFilter.swift */,
 			);
 			path = Helpers;
 			sourceTree = "<group>";
@@ -2271,6 +2274,7 @@
 				38C4D33725E9A1A300D30B77 /* DispatchQueue+Extensions.swift in Sources */,
 				F90692CF274B999A0037068D /* HealthKitDataFlow.swift in Sources */,
 				3862CC2E2743F9F700BF832C /* CalendarManager.swift in Sources */,
+				CEA4F62329BE10F70011ADF7 /* SavitzkyGolayFilter.swift in Sources */,
 				38B4F3C325E2A20B00E76A18 /* PumpSetupView.swift in Sources */,
 				38E4453C274E411700EC9A94 /* Disk+Codable.swift in Sources */,
 				382C134B25F14E3700715CE1 /* BGTargets.swift in Sources */,

+ 6 - 0
FreeAPS.xcworkspace/contents.xcworkspacedata

@@ -5,6 +5,12 @@
       location = "container:"
       name = "CoreDataClassesAndProperties">
       <FileRef
+         location = "group:Presets+CoreDataClass.swift">
+      </FileRef>
+      <FileRef
+         location = "group:Presets+CoreDataProperties.swift">
+      </FileRef>
+      <FileRef
          location = "group:InsulinDistribution+CoreDataClass.swift">
       </FileRef>
       <FileRef

Разница между файлами не показана из-за своего большого размера
+ 1 - 1
FreeAPS/Resources/javascript/bundle/determine-basal.js


+ 29 - 33
FreeAPS/Sources/APS/APSManager.swift

@@ -974,7 +974,7 @@ final class BaseAPSManager: APSManager, Injectable {
             minimumLoopTime = 0.0
         }
 
-        var glucose: [Readings] = []
+        var glucose = [Readings]()
 
         var firstElementTime = Date()
         var lastElementTime = Date()
@@ -1163,16 +1163,19 @@ final class BaseAPSManager: APSManager, Injectable {
 
         // MARK: Save to Median to CoreData
 
-        let saveMedianToCoreData = BGmedian(context: 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
-        saveMedianToCoreData.median_90 = roundDecimal(Decimal(medianCalculation(array: bgArray_90_)), 1) as NSDecimalNumber
-
         coredataContext.perform {
+            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
+            saveMedianToCoreData.median_90 = self.roundDecimal(
+                Decimal(self.medianCalculation(array: bgArray_90_)),
+                1
+            ) as NSDecimalNumber
+
             try? self.coredataContext.save()
         }
 
@@ -1183,17 +1186,15 @@ final class BaseAPSManager: APSManager, Injectable {
             total: roundDecimal(NGSPa1CStatisticValue_total, 1)
         )
 
-        let saveHbA1c = HbA1c(context: 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
-        saveHbA1c.hba1c_90 = NGSPa1CStatisticValue_90 as NSDecimalNumber
-
-        // MARK: Save to HbA1c to CoreData
-
         coredataContext.perform {
+            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
+            saveHbA1c.hba1c_90 = NGSPa1CStatisticValue_90 as NSDecimalNumber
+
             try? self.coredataContext.save()
         }
 
@@ -1279,24 +1280,19 @@ final class BaseAPSManager: APSManager, Injectable {
             total: roundDecimal(bg_total, 1)
         )
 
-        let saveAverages = BGaverages(context: 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
-        saveAverages.average_90 = bg_90 as NSDecimalNumber
-
-        // MARK: Save to HbA1c to CoreData
-
         coredataContext.perform {
+            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
+            saveAverages.average_90 = bg_90 as NSDecimalNumber
+
             try? self.coredataContext.save()
         }
 
         let avg = Averages(Average: avgs, Median: median)
-
-        // MARK: Fetch InsulinDuration from CoreData
-
         var insulinDistribution = [InsulinDistribution]()
 
         var insulin = Ins(

+ 2 - 2
FreeAPS/Sources/APS/CGM/GlucoseSimulatorSource.swift

@@ -136,7 +136,7 @@ class IntelligentGenerator: BloodGlucoseGenerator {
         trandsStepDirection = getDirection(fromGlucose: previousGlucose, toGlucose: currentGlucose).rawValue
         let glucose = BloodGlucose(
             _id: UUID().uuidString,
-            sgv: nil,
+            sgv: currentGlucose,
             direction: BloodGlucose.Direction(rawValue: trandsStepDirection),
             date: Decimal(Int(date.timeIntervalSince1970) * 1000),
             dateString: date,
@@ -183,7 +183,7 @@ class IntelligentGenerator: BloodGlucoseGenerator {
 
     private func makeStepInTrend() {
         currentGlucose +=
-            Int(Double((trendTargetValue - currentGlucose) / trendStepsLeft) * [0.3, 0.6, 1, 1.3, 1.6].randomElement()!)
+        Int(Double((trendTargetValue - currentGlucose) / trendStepsLeft) * [0.3, 0.6, 1, 1.3, 1.6, 2.0].randomElement()!)
         trendStepsLeft -= 1
         if trendStepsLeft == 0 {
             generateNewTrend()

+ 15 - 0
FreeAPS/Sources/APS/FetchGlucoseManager.swift

@@ -130,6 +130,21 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         }
         debug(.deviceManager, "New glucose found")
 
+        // filter the data if it is the case
+        if settingsManager.settings.smoothGlucose {
+            // limit to 30 minutes of previous BG Data
+            let oldGlucoses = glucoseStorage.recent().filter {
+                $0.dateString.addingTimeInterval(31 * 60) > Date()
+            }
+            var smoothedValues = oldGlucoses + filtered
+            // smooth with 3 repeats
+            for _ in 1 ... 3 {
+                smoothedValues.smoothSavitzkyGolayQuaDratic(withFilterWidth: 3)
+            }
+            // find the new values only
+            filtered = smoothedValues.filter { $0.dateString > syncDate }
+        }
+
         glucoseStorage.storeGlucose(filtered)
 
         deviceDataManager.heartbeat(date: Date())

+ 6 - 0
FreeAPS/Sources/Application/FreeAPSApp.swift

@@ -1,10 +1,14 @@
+import CoreData
 import SwiftUI
 import Swinject
 
 @main struct FreeAPSApp: App {
     @Environment(\.scenePhase) var scenePhase
+
     @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
 
+    @StateObject var dataController = CoreDataStack.shared
+
     // Dependencies Assembler
     // contain all dependencies Assemblies
     // TODO: Remove static key after update "Use Dependencies" logic
@@ -53,6 +57,8 @@ import Swinject
     var body: some Scene {
         WindowGroup {
             Main.RootView(resolver: resolver)
+
+                .environment(\.managedObjectContext, dataController.persistentContainer.viewContext)
         }
         .onChange(of: scenePhase) { newScenePhase in
             debug(.default, "APPLICATION PHASE: \(newScenePhase)")

+ 6 - 2
FreeAPS/Sources/Helpers/CoreDataStack.swift

@@ -1,8 +1,8 @@
 import CoreData
 import Foundation
 
-class CoreDataStack {
-    private init() {}
+class CoreDataStack: ObservableObject {
+    init() {}
 
     static let shared = CoreDataStack()
 
@@ -33,4 +33,8 @@ class CoreDataStack {
             }
         }
     }
+
+    func delete(obj: NSManagedObject) {
+        persistentContainer.viewContext.delete(obj)
+    }
 }

+ 172 - 0
FreeAPS/Sources/Helpers/SavitzkyGolayFilter.swift

@@ -0,0 +1,172 @@
+import Foundation
+
+/// allowed values are 0, 1, 2 or 3. It's the index in coefficients
+private var coefficientsRowToUse = 3
+
+/// Savitzky Golay coefficients
+private let coefficients = [
+    [-3.0, 12.0, 17.0, 12.0, -3.0],
+    [-2.0, 3.0, 6.0, 7.0, 6.0, 3.0, -2.0],
+    [-21.0, 14.0, 39.0, 54.0, 59.0, 54.0, 39.0, 14.0, -21.0],
+    [-36.0, 9.0, 44.0, 69.0, 84.0, 89.0, 84.0, 69.0, 44.0, 9.0, -36.0]
+]
+
+/// an array with elements of a type that conforms to Smoothable, can be filtered using  the Savitzky Golay algorithm
+protocol SavitzkyGolaySmoothable {
+    /// value to be smoothed
+    var value: Double { get set }
+}
+
+/// local help class
+private class IsSmoothable: SavitzkyGolaySmoothable {
+    var value: Double = 0.0
+
+    init(withValue value: Double = 0.0) {
+        self.value = value
+    }
+}
+
+extension Array where Element: SavitzkyGolaySmoothable {
+    /// - apply Savitzky Golay filter
+    /// - before applying the filter, the array will be prepended and append with a number of elements equal to the filterwidth, filterWidth default 5. Allowed values are 5, 4, 3, 2. If any other value is assigned, then 5 will be used
+    /// - ...continue with 5 here in the explanation ...
+    /// - for the 5 last elements and 5 first elements, a regression is done. This regression is done used to give values to the 5 prepended and appended values. Which means it's as if we draw a line through the first 5 and 5 last original values, and use this line to give values to the 5 prepended and appended values
+    /// - the 5 prepended and appended values are then used in the filter algorithm, which means we can also filter the original 5 first and last elements
+    /// see also example https://github.com/JohanDegraeve/xdripswift/wiki/Libre-value-smoothing
+    mutating func smoothSavitzkyGolayQuaDratic(withFilterWidth filterWidth: Int = 5) {
+        // filterWidthToUse is the value of filterWidth to use in the algorithm. By default filterWidthToUse = parameter value filterWidth
+        var filterWidthToUse = filterWidth
+
+        // calculate coefficientsRowToUse based on filterWdith
+        switch filterWidth {
+        case 5:
+            coefficientsRowToUse = 3
+
+        case 4:
+            coefficientsRowToUse = 2
+
+        case 3:
+            coefficientsRowToUse = 1
+
+        case 2:
+            coefficientsRowToUse = 0
+
+        default:
+            // invalid filterWidth was given in parameterList, use default value
+            coefficientsRowToUse = 3
+
+            filterWidthToUse = 5
+        }
+
+        // using 5 here in the comments as value for filterWidthToUse
+
+        // the amount of elements must be at least 5. If that's not the case then don't apply any smoothing
+        guard count >= filterWidthToUse else { return }
+
+        // create a new array, to which we will prepend and append 5 elements so that we can do also smoothing for the 5 last and 5 first values of the input array (which is self)
+        // the 5 elements will be estimated by doing linear regression of the first 5 and last 5 elements of the original input array respectively
+        // this is only a temporary array, but it will hold the elements of the original array, those elements will get a new value when doing the smoothing
+        var tempArray = [SavitzkyGolaySmoothable]()
+        for element in self {
+            tempArray.append(element)
+        }
+
+        // now prepend and append with 5 elements, each with a default value 0.0
+        for _ in 0 ..< filterWidthToUse {
+            tempArray.insert(IsSmoothable(), at: 0)
+            tempArray.append(IsSmoothable())
+        }
+
+        // so now we have tempArray, of length size of original array + 2 * 5
+        // the first 5 and the last 5 elements are of type IsSmoothable with value 0
+
+        // - indicesArray is a help array needed for the function linearRegressionCreator
+        // - this will be the first parameter in the call to the linearRegression function, in fact it's an array of IsSmoothable with length = length of tempArray
+        // - we give each IsSmoothable the value of the index, meaning from 0 up to (length of tempArray) - 1
+        // - in fact it's not really smoothable, it's just because we use isSmoothable in function linearRegressionCreator
+        var indicesArray = [SavitzkyGolaySmoothable]()
+        for index in 0 ..< (count + (filterWidthToUse * 2)) {
+            indicesArray.append(IsSmoothable(withValue: Double(index)))
+        }
+
+        /// - this is a piece of code that we will execute two times, once for the firs 5 elements, then for the last 5, so we put it in a closure variable
+        /// - it calculates the regression function (which is nothing else but doing y = intercept + slope*x) for range defined by predictorRange in tempArray. It will be used for the 5 first and 5 last real values, ie the 5 first and 5 last real glucose values
+        /// - then executes the regression for every element in the range defined by targetRange, again in tempArray
+        let doRegression = { (predictorRange: Range<Int>, targetRange: Range<Int>) in
+
+            // calculate the linearRegression function
+            let linearRegression = linearRegressionCreator(indicesArray[predictorRange], tempArray[predictorRange])
+
+            // ready to do the linear regression for the targetRange in tempArray
+            for index in targetRange {
+                tempArray[index].value = linearRegression(indicesArray[index].value)
+            }
+        }
+
+        // now do the regression for the 5 first elements
+        doRegression(filterWidthToUse ..< (filterWidthToUse * 2), 0 ..< filterWidthToUse)
+
+        // now do the regression for the 5 last elements
+        doRegression(
+            (tempArray.count - filterWidthToUse * 2) ..< (tempArray.count - filterWidthToUse),
+            (tempArray.count - filterWidthToUse) ..< tempArray.count
+        )
+
+        // now start filtering
+
+        // initialize array that will hold the resulting filtered values
+        var filteredValues = [Double]()
+
+        // calculate divider
+        let divider = coefficients[coefficientsRowToUse].reduce(0, { x, y in
+            x + y
+        })
+
+        // filter each original value
+        for _ in 0 ..< count {
+            // add a new element to filteredValues, start value is 0.0
+            // this new value will be the last element, so we access it with index filteredValues.count - 1
+            filteredValues.append(0.0)
+
+            // iterate through the coefficients
+            for (index, coefficient) in coefficients[coefficientsRowToUse].enumerated() {
+                filteredValues[filteredValues.count - 1] = filteredValues[filteredValues.count - 1] + coefficient *
+                    tempArray[index + filteredValues.count - 1].value
+            }
+
+            filteredValues[filteredValues.count - 1] = filteredValues[filteredValues.count - 1] / divider
+        }
+
+        // now assign the new values to the original objects
+        for (index, _) in enumerated() {
+            self[index].value = filteredValues[index]
+        }
+    }
+}
+
+/// source https://github.com/raywenderlich/swift-algorithm-club/tree/master/Linear%20Regression
+private func multiply(
+    _ a: ArraySlice<SavitzkyGolaySmoothable>,
+    _ b: ArraySlice<SavitzkyGolaySmoothable>
+) -> ArraySlice<SavitzkyGolaySmoothable> {
+    zip(a, b).map({ IsSmoothable(withValue: $0.value * $1.value) })[0 ..< a.count]
+}
+
+/// source https://github.com/raywenderlich/swift-algorithm-club/tree/master/Linear%20Regression
+private func average(_ input: ArraySlice<SavitzkyGolaySmoothable>) -> Double {
+    (input.reduce(IsSmoothable(), { (x: SavitzkyGolaySmoothable, y: SavitzkyGolaySmoothable) in
+        IsSmoothable(withValue: x.value + y.value) })).value / Double(input.count)
+}
+
+/// source https://github.com/raywenderlich/swift-algorithm-club/tree/master/Linear%20Regression
+private func linearRegressionCreator(
+    _ xs: ArraySlice<SavitzkyGolaySmoothable>,
+    _ ys: ArraySlice<SavitzkyGolaySmoothable>
+) -> (Double) -> Double {
+    let sum1 = average(multiply(ys, xs)) - average(xs) * average(ys)
+    let sum2 = average(multiply(xs, xs)) - pow(average(xs), 2)
+    let slope = sum1 / sum2
+    let intercept = average(ys) - slope * average(xs)
+
+    return { x in intercept + slope * x }
+}

+ 21 - 0
FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings

@@ -1087,6 +1087,27 @@ Enact a temp Basal or a temp target */
 /* Section in settings */
 "Protein / Fat" = "Protein / Fat";
 
+/* */
+"Meal Presets" = "Meal Presets";
+
+/* */
+"Empty" = "Empty";
+
+/* */
+"Delete Selected Preset" = "Delete Selected Preset";
+
+/* */
+"Enter Meal Preset Name" = "Enter Meal Preset Name";
+
+/* */
+"Name Of Dish" = "Name Of Dish";
+
+/* Save Carbs and continue to bolus recommendation */
+"Save and continue" = "Save and continue";
+
+/* */
+"Save as Preset" = "Save as Preset"
+
 /* -------------------------------------------------------------------------------------------
   DASH strings
 */

+ 11 - 0
FreeAPS/Sources/Models/BloodGlucose.swift

@@ -79,3 +79,14 @@ extension Double {
         Decimal(self) / GlucoseUnits.exchangeRate
     }
 }
+
+extension BloodGlucose: SavitzkyGolaySmoothable {
+    var value: Double {
+        get {
+            Double(glucose ?? 0)
+        }
+        set {
+            glucose = Int(newValue)
+        }
+    }
+}

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

@@ -30,6 +30,7 @@ struct FreeAPSSettings: JSON, Equatable {
     var timeCap: Int = 8
     var minuteInterval: Int = 30
     var delay: Int = 60
+    var smoothGlucose: Bool = false
 }
 
 extension FreeAPSSettings: Decodable {
@@ -157,6 +158,10 @@ extension FreeAPSSettings: Decodable {
             settings.displayStatistics = displayStatistics
         }
 
+        if let smoothGlucose = try? container.decode(Bool.self, forKey: .smoothGlucose) {
+            settings.smoothGlucose = smoothGlucose
+        }
+
         self = settings
     }
 }

+ 19 - 0
FreeAPS/Sources/Modules/AddCarbs/AddCarbsStateModel.swift

@@ -1,3 +1,4 @@
+import CoreData
 import SwiftUI
 
 extension AddCarbs {
@@ -11,6 +12,11 @@ extension AddCarbs {
         @Published var fat: Decimal = 0
         @Published var carbsRequired: Decimal?
         @Published var useFPU: Bool = false
+        @Published var dish: String = ""
+        @Published var selection: Presets?
+
+        let coredataContext = CoreDataStack.shared.persistentContainer.viewContext // .newBackgroundContext()
+        @Environment(\.managedObjectContext) var moc
 
         override func subscribe() {
             carbsRequired = provider.suggestion?.carbsReq
@@ -51,6 +57,8 @@ extension AddCarbs {
                 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))
                 // Number of equivalents
                 var numberOfEquivalents = carbEquivalents / equivalent
                 // Only use delay in first loop
@@ -100,5 +108,16 @@ extension AddCarbs {
                 showModal(for: .bolus(waitForSuggestion: true))
             }
         }
+
+        func deletePreset() {
+            if selection != nil {
+                try? coredataContext.delete(selection!)
+                try? coredataContext.save()
+                carbs = 0
+                fat = 0
+                protein = 0
+            }
+            selection = nil
+        }
     }
 }

+ 83 - 6
FreeAPS/Sources/Modules/AddCarbs/View/AddCarbsRootView.swift

@@ -1,3 +1,4 @@
+import CoreData
 import SwiftUI
 import Swinject
 
@@ -5,11 +6,21 @@ extension AddCarbs {
     struct RootView: BaseView {
         let resolver: Resolver
         @StateObject var state = StateModel()
+        @State var dish: String = ""
+        @State var isPromtPresented = false
+        @State var saved = false
+
+        @FetchRequest(
+            entity: Presets.entity(),
+            sortDescriptors: [NSSortDescriptor(key: "dish", ascending: true)]
+        ) var carbPresets: FetchedResults<Presets>
+
+        @Environment(\.managedObjectContext) var moc
 
         private var formatter: NumberFormatter {
             let formatter = NumberFormatter()
             formatter.numberStyle = .decimal
-            formatter.maximumFractionDigits = 0
+            formatter.maximumFractionDigits = 1
             return formatter
         }
 
@@ -33,8 +44,6 @@ extension AddCarbs {
                             Text("grams").foregroundColor(.secondary)
                         }.padding(.vertical)
 
-                        // MARK: Adding Protein and Fat. Test
-
                         if state.useFPU {
                             HStack {
                                 Text("Protein").foregroundColor(.loopRed).fontWeight(.thin)
@@ -46,6 +55,7 @@ extension AddCarbs {
                                     autofocus: false,
                                     cleanInput: true
                                 ).foregroundColor(.loopRed)
+
                                 Text("grams").foregroundColor(.secondary)
                             }
                             HStack {
@@ -60,20 +70,87 @@ extension AddCarbs {
                                 )
                                 Text("grams").foregroundColor(.secondary)
                             }
+
+                            HStack {
+                                Button {
+                                    isPromtPresented = true
+                                }
+                                label: { Text("Save as Preset") }
+                            }
+                            .frame(maxWidth: .infinity, alignment: .trailing)
+                            .disabled(state.carbs <= 0 && state.fat <= 0 && state.protein <= 0)
+                            .popover(isPresented: $isPromtPresented) {
+                                presetPopover
+                            }
                         }
                         DatePicker("Date", selection: $state.date)
                     }
                 }
+
                 Section {
                     Button { state.add() }
-                    label: { Text("Add") }
+                    label: { Text("Save and continue") }
                         .disabled(state.carbs <= 0 && state.fat <= 0 && state.protein <= 0)
                 }
+
+                if state.useFPU {
+                    mealPresets
+                }
             }
             .onAppear(perform: configureView)
-            .navigationTitle("Add Carbs")
-            .navigationBarTitleDisplayMode(.automatic)
             .navigationBarItems(leading: Button("Close", action: state.hideModal))
         }
+
+        var presetPopover: some View {
+            Form {
+                Section(header: Text("Enter Meal Preset Name")) {
+                    TextField("Name Of Dish", text: $dish)
+                    Button {
+                        saved = true
+                        if dish != "", saved {
+                            let preset = Presets(context: moc)
+                            preset.dish = dish
+                            preset.fat = state.fat as NSDecimalNumber
+                            preset.protein = state.protein as NSDecimalNumber
+                            preset.carbs = state.carbs as NSDecimalNumber
+                            try? moc.save()
+                            state.selection = preset
+                            saved = false
+                            isPromtPresented = false
+                        }
+                    }
+                    label: { Text("Save") }
+                    Button {
+                        dish = ""
+                        saved = false
+                        isPromtPresented = false }
+                    label: { Text("Cancel") }
+                }
+            }
+        }
+
+        var mealPresets: some View {
+            Section {
+                VStack {
+                    Picker("Meal Presets", selection: $state.selection) {
+                        Text("Empty").tag(nil as Presets?)
+                        ForEach(carbPresets, id: \.self) { (preset: Presets) in
+                            Text(preset.dish ?? "").tag(preset as Presets?)
+                        }
+                    }
+                    .pickerStyle(.automatic)
+                    ._onBindingChange($state.selection) { _ in
+                        state.carbs = ((state.selection?.carbs ?? 0) as NSDecimalNumber) as Decimal
+                        state.fat = ((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal
+                        state.protein = ((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal
+                    }
+                }
+                Button {
+                    state.deletePreset()
+                }
+                label: { Text("Delete Selected Preset") }
+                    .disabled(state.selection == nil)
+            }
+        }
     }
 }

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

@@ -14,6 +14,7 @@ extension CGM {
         @Published var cgm: CGMType = .nightscout
         // @Published var transmitterID = ""
         @Published var uploadGlucose = false
+        @Published var smoothGlucose = false
         @Published var createCalendarEvents = false
         @Published var calendarIDs: [String] = []
         @Published var currentCalendarID: String = ""
@@ -39,6 +40,8 @@ extension CGM {
                 }
             })
 
+            subscribeSetting(\.smoothGlucose, on: $smoothGlucose, initial: { smoothGlucose = $0 })
+
             $cgm
                 .removeDuplicates()
                 .sink { [weak self] value in

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

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

+ 3 - 0
FreeAPS/Sources/Modules/Home/HomeStateModel.swift

@@ -53,6 +53,7 @@ extension Home {
         @Published var alarm: GlucoseAlarm?
         @Published var animatedBackground = false
         @Published var manualTempBasal = false
+        @Published var smooth = false
 
         override func subscribe() {
             setupGlucose()
@@ -83,6 +84,7 @@ extension Home {
             manualTempBasal = apsManager.isManualTempBasal
             setStatusTitle()
             setupCurrentTempTarget()
+            smooth = settingsManager.settings.smoothGlucose
 
             broadcaster.register(GlucoseObserver.self, observer: self)
             broadcaster.register(SuggestionObserver.self, observer: self)
@@ -386,6 +388,7 @@ extension Home.StateModel:
         units = settingsManager.settings.units
         animatedBackground = settingsManager.settings.animatedBackground
         manualTempBasal = apsManager.isManualTempBasal
+        smooth = settingsManager.settings.smoothGlucose
         setupGlucose()
         setupStatistics()
     }

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

@@ -50,9 +50,11 @@ struct MainChartView: View {
     @Binding var carbs: [CarbsEntry]
     @Binding var timerDate: Date
     @Binding var units: GlucoseUnits
+    @Binding var smooth: Bool
 
     @State var didAppearTrigger = false
     @State private var glucoseDots: [CGRect] = []
+    @State private var unSmoothedGlucoseDots: [CGRect] = []
     @State private var predictionDots: [PredictionType: [CGRect]] = [:]
     @State private var bolusDots: [DotInfo] = []
     @State private var bolusPath = Path()
@@ -249,6 +251,7 @@ struct MainChartView: View {
                     carbsView(fullSize: fullSize)
                     fpuView(fullSize: fullSize)
                     bolusView(fullSize: fullSize)
+                    if smooth { unSmoothedGlucoseView(fullSize: fullSize) }
                     glucoseView(fullSize: fullSize)
                     predictionsView(fullSize: fullSize)
                 }
@@ -320,6 +323,27 @@ struct MainChartView: View {
         }
     }
 
+    private func unSmoothedGlucoseView(fullSize: CGSize) -> some View {
+        Path { path in
+            var lines: [CGPoint] = []
+            for rect in unSmoothedGlucoseDots {
+                lines.append(CGPoint(x: rect.midX, y: rect.midY))
+                path.addEllipse(in: rect)
+            }
+            path.addLines(lines)
+        }
+        .stroke(Color.loopGray, lineWidth: 0.5)
+        .onChange(of: glucose) { _ in
+            update(fullSize: fullSize)
+        }
+        .onChange(of: didAppearTrigger) { _ in
+            update(fullSize: fullSize)
+        }
+        .onReceive(Foundation.NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
+            update(fullSize: fullSize)
+        }
+    }
+
     private func bolusView(fullSize: CGSize) -> some View {
         ZStack {
             bolusPath
@@ -445,6 +469,7 @@ extension MainChartView {
         calculatePredictionDots(fullSize: fullSize, type: .zt)
         calculatePredictionDots(fullSize: fullSize, type: .uam)
         calculateGlucoseDots(fullSize: fullSize)
+        calculateUnSmoothedGlucoseDots(fullSize: fullSize)
         calculateBolusDots(fullSize: fullSize)
         calculateCarbsDots(fullSize: fullSize)
         calculateFPUsDots(fullSize: fullSize)
@@ -469,6 +494,22 @@ extension MainChartView {
         }
     }
 
+    private func calculateUnSmoothedGlucoseDots(fullSize: CGSize) {
+        calculationQueue.async {
+            let dots = glucose.concurrentMap { value -> CGRect in
+                let position = UnSmoothedGlucoseToCoordinate(value, fullSize: fullSize)
+                return CGRect(x: position.x - 2, y: position.y - 2, width: 4, height: 4)
+            }
+
+            let range = self.getGlucoseYRange(fullSize: fullSize)
+
+            DispatchQueue.main.async {
+                glucoseYGange = range
+                unSmoothedGlucoseDots = dots
+            }
+        }
+    }
+
     private func calculateBolusDots(fullSize: CGSize) {
         calculationQueue.async {
             let dots = boluses.map { value -> DotInfo in
@@ -879,6 +920,13 @@ extension MainChartView {
         return CGPoint(x: x, y: y)
     }
 
+    private func UnSmoothedGlucoseToCoordinate(_ glucoseEntry: BloodGlucose, fullSize: CGSize) -> CGPoint {
+        let x = timeToXCoordinate(glucoseEntry.dateString.timeIntervalSince1970, fullSize: fullSize)
+        let y = glucoseToYCoordinate(glucoseEntry.sgv ?? glucoseEntry.glucose ?? 0, fullSize: fullSize)
+
+        return CGPoint(x: x, y: y)
+    }
+
     private func predictionToCoordinate(_ pred: Int, fullSize: CGSize, index: Int) -> CGPoint {
         guard let deliveredAt = suggestion?.deliverAt else {
             return .zero

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

@@ -566,7 +566,8 @@ extension Home {
                     tempTargets: $state.tempTargets,
                     carbs: $state.carbs,
                     timerDate: $state.timerDate,
-                    units: $state.units
+                    units: $state.units,
+                    smooth: $state.smooth
                 )
             }
             .padding(.bottom)

+ 19 - 2
FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift

@@ -280,16 +280,33 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsObserver {
                     .compactMap { item -> InsulinBasal? in
                         let nextElementEventIndex = item.offset + 1
                         guard basalEvents.count > nextElementEventIndex else { return nil }
+
+                        var minimalDose = self.settingsManager.preferences.bolusIncrement
+                        if (minimalDose != 0.05) || (minimalDose != 0.025) {
+                            minimalDose = Decimal(0.05)
+                        }
+
                         let nextBasalEvent = basalEvents[nextElementEventIndex]
                         let secondsOfCurrentBasal = nextBasalEvent.timestamp.timeIntervalSince(item.element.timestamp)
                         let amount = Decimal(secondsOfCurrentBasal / 3600) * (item.element.rate ?? 0)
+                        let incrementsRaw = amount / minimalDose
+
+                        var amountRounded: Decimal
+                        if incrementsRaw >= 1 {
+                            let incrementsRounded = floor(Double(incrementsRaw))
+                            amountRounded = Decimal(round(incrementsRounded * Double(minimalDose) * 100_000.0) / 100_000.0)
+                        } else {
+                            amountRounded = 0
+                        }
+
                         let id = String(item.element.id.dropFirst())
-                        guard amount > 0,
+                        guard amountRounded >= 0,
                               id != ""
                         else { return nil }
+
                         return InsulinBasal(
                             id: id,
-                            amount: amount,
+                            amount: amountRounded,
                             startDelivery: item.element.timestamp,
                             endDelivery: nextBasalEvent.timestamp
                         )

+ 2 - 2
InsulinDistribution+CoreDataProperties.swift

@@ -7,8 +7,8 @@ public extension InsulinDistribution {
     }
 
     @NSManaged var bolus: NSDecimalNumber?
-    @NSManaged var tempBasal: NSDecimalNumber?
-    @NSManaged var scheduledBasal: NSDecimalNumber?
     @NSManaged var date: Date?
+    @NSManaged var scheduledBasal: NSDecimalNumber?
+    @NSManaged var tempBasal: NSDecimalNumber?
     @NSManaged var insulin: Oref0Suggestion?
 }

+ 1 - 1
LoopStatRecord+CoreDataProperties.swift

@@ -8,6 +8,6 @@ public extension LoopStatRecord {
 
     @NSManaged var duration: Double
     @NSManaged var end: Date?
-    @NSManaged var start: Date?
     @NSManaged var loopStatus: String?
+    @NSManaged var start: Date?
 }

+ 17 - 17
Oref0Suggestion+CoreDataProperties.swift

@@ -6,24 +6,8 @@ public extension Oref0Suggestion {
         NSFetchRequest<Oref0Suggestion>(entityName: "Oref0Suggestion")
     }
 
-    @NSManaged var computedTDD: NSSet?
     @NSManaged var computedInsulinDistribution: NSSet?
-}
-
-// MARK: Generated accessors for computedTDD
-
-public extension Oref0Suggestion {
-    @objc(addComputedTDDObject:)
-    @NSManaged func addToComputedTDD(_ value: TDD)
-
-    @objc(removeComputedTDDObject:)
-    @NSManaged func removeFromComputedTDD(_ value: TDD)
-
-    @objc(addComputedTDD:)
-    @NSManaged func addToComputedTDD(_ values: NSSet)
-
-    @objc(removeComputedTDD:)
-    @NSManaged func removeFromComputedTDD(_ values: NSSet)
+    @NSManaged var computedTDD: NSSet?
 }
 
 // MARK: Generated accessors for computedInsulinDistribution
@@ -41,3 +25,19 @@ public extension Oref0Suggestion {
     @objc(removeComputedInsulinDistribution:)
     @NSManaged func removeFromComputedInsulinDistribution(_ values: NSSet)
 }
+
+// MARK: Generated accessors for computedTDD
+
+public extension Oref0Suggestion {
+    @objc(addComputedTDDObject:)
+    @NSManaged func addToComputedTDD(_ value: TDD)
+
+    @objc(removeComputedTDDObject:)
+    @NSManaged func removeFromComputedTDD(_ value: TDD)
+
+    @objc(addComputedTDD:)
+    @NSManaged func addToComputedTDD(_ values: NSSet)
+
+    @objc(removeComputedTDD:)
+    @NSManaged func removeFromComputedTDD(_ values: NSSet)
+}

+ 4 - 0
Presets+CoreDataClass.swift

@@ -0,0 +1,4 @@
+import CoreData
+import Foundation
+
+@objc(Presets) public class Presets: NSManagedObject {}

+ 13 - 0
Presets+CoreDataProperties.swift

@@ -0,0 +1,13 @@
+import CoreData
+import Foundation
+
+public extension Presets {
+    @nonobjc class func fetchRequest() -> NSFetchRequest<Presets> {
+        NSFetchRequest<Presets>(entityName: "Presets")
+    }
+
+    @NSManaged var carbs: NSDecimalNumber?
+    @NSManaged var dish: String?
+    @NSManaged var fat: NSDecimalNumber?
+    @NSManaged var protein: NSDecimalNumber?
+}