Pārlūkot izejas kodu

Initial take on refactoring glucose smoothing
* Remove Savitzky-Golay
* Change storage workflow
* Port over exponential smoothing via AAPS master

Deniz Cengiz 2 mēneši atpakaļ
vecāks
revīzija
8ffad42f65

+ 13 - 2
Model/Classes+Properties/GlucoseStored+CoreDataClass.swift

@@ -1,4 +1,15 @@
-import CoreData
+//
+//  GlucoseStored+CoreDataClass.swift
+//  Trio
+//
+//  Created by Cengiz Deniz on 24.02.26.
+//
+//
+
 import Foundation
+import CoreData
+
+@objc(GlucoseStored)
+public class GlucoseStored: NSManagedObject {
 
-@objc(GlucoseStored) public class GlucoseStored: NSManagedObject {}
+}

+ 27 - 13
Model/Classes+Properties/GlucoseStored+CoreDataProperties.swift

@@ -1,19 +1,33 @@
-import CoreData
+//
+//  GlucoseStored+CoreDataProperties.swift
+//  Trio
+//
+//  Created by Cengiz Deniz on 24.02.26.
+//
+//
+
 import Foundation
+import CoreData
+
 
-public extension GlucoseStored {
-    @nonobjc class func fetchRequest() -> NSFetchRequest<GlucoseStored> {
-        NSFetchRequest<GlucoseStored>(entityName: "GlucoseStored")
+extension GlucoseStored {
+
+    @nonobjc public class func fetchRequest() -> NSFetchRequest<GlucoseStored> {
+        return NSFetchRequest<GlucoseStored>(entityName: "GlucoseStored")
     }
 
-    @NSManaged var date: Date?
-    @NSManaged var direction: String?
-    @NSManaged var glucose: Int16
-    @NSManaged var id: UUID?
-    @NSManaged var isManual: Bool
-    @NSManaged var isUploadedToNS: Bool
-    @NSManaged var isUploadedToHealth: Bool
-    @NSManaged var isUploadedToTidepool: Bool
+    @NSManaged public var date: Date?
+    @NSManaged public var direction: String?
+    @NSManaged public var glucose: Int16
+    @NSManaged public var id: UUID?
+    @NSManaged public var isManual: Bool
+    @NSManaged public var isUploadedToHealth: Bool
+    @NSManaged public var isUploadedToNS: Bool
+    @NSManaged public var isUploadedToTidepool: Bool
+    @NSManaged public var smoothedGlucose: NSDecimalNumber?
+
 }
 
-extension GlucoseStored: Identifiable {}
+extension GlucoseStored : Identifiable {
+
+}

+ 2 - 1
Model/TrioCoreDataPersistentContainer.xcdatamodeld/TrioCoreDataPersistentContainer.xcdatamodel/contents

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="24512" systemVersion="25B78" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
+<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23788.4" systemVersion="25B78" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
     <entity name="BolusStored" representedClassName="BolusStored" syncable="YES">
         <attribute name="amount" optional="YES" attributeType="Decimal" defaultValueString="0"/>
         <attribute name="isExternal" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
@@ -78,6 +78,7 @@
         <attribute name="isUploadedToHealth" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
         <attribute name="isUploadedToNS" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
         <attribute name="isUploadedToTidepool" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
+        <attribute name="smoothedGlucose" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
         <fetchIndex name="byDate">
             <fetchIndexElement property="date" type="Binary" order="ascending"/>
         </fetchIndex>

+ 12 - 12
Trio.xcodeproj/project.pbxproj

@@ -503,7 +503,6 @@
 		CE95BF622BA7715900DC3DE3 /* MockKitUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3818AA4F274C26A300843DB3 /* MockKitUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		CE95BF632BA771BE00DC3DE3 /* LoopTestingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3818AA70274C278200843DB3 /* LoopTestingKit.framework */; };
 		CE95BF642BA771BE00DC3DE3 /* LoopTestingKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3818AA70274C278200843DB3 /* LoopTestingKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
-		CEA4F62329BE10F70011ADF7 /* SavitzkyGolayFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEA4F62229BE10F70011ADF7 /* SavitzkyGolayFilter.swift */; };
 		CEB434E328B8F9DB00B70274 /* BluetoothStateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB434E228B8F9DB00B70274 /* BluetoothStateManager.swift */; };
 		CEB434E528B8FF5D00B70274 /* UIColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB434E428B8FF5D00B70274 /* UIColor.swift */; };
 		CEB434E728B9053300B70274 /* LoopUIColorPalette+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB434E628B9053300B70274 /* LoopUIColorPalette+Default.swift */; };
@@ -623,6 +622,7 @@
 		DD9ECB712CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6E2CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift */; };
 		DD9ECB722CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6F2CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift */; };
 		DD9ECB742CA9A0C300AA7C45 /* RemoteControlConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB732CA9A0C300AA7C45 /* RemoteControlConfig.swift */; };
+		DDA40BBA2F4DB18800257798 /* AlgorithmGlucose.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA40BB92F4DB18100257798 /* AlgorithmGlucose.swift */; };
 		DDA6E2502D22187500C2988C /* ChartLegendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6E24F2D22187500C2988C /* ChartLegendView.swift */; };
 		DDA6E2852D2361F800C2988C /* LoopStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6E2842D2361F800C2988C /* LoopStatusView.swift */; };
 		DDA6E3202D258E0500C2988C /* OverrideHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6E31F2D258E0500C2988C /* OverrideHelpView.swift */; };
@@ -652,6 +652,8 @@
 		DDD78A912DC4064800AC63F3 /* carbhistory.json in Resources */ = {isa = PBXBuildFile; fileRef = DDD78A902DC4064800AC63F3 /* carbhistory.json */; };
 		DDD78AD92DC421B500AC63F3 /* enacted.json in Resources */ = {isa = PBXBuildFile; fileRef = DDD78AD72DC421B500AC63F3 /* enacted.json */; };
 		DDD78ADA2DC421B500AC63F3 /* suggested.json in Resources */ = {isa = PBXBuildFile; fileRef = DDD78AD82DC421B500AC63F3 /* suggested.json */; };
+		DDD7C8C12F4DB45400E5CF09 /* GlucoseStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD7C8BF2F4DB45400E5CF09 /* GlucoseStored+CoreDataClass.swift */; };
+		DDD7C8C22F4DB45400E5CF09 /* GlucoseStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD7C8C02F4DB45400E5CF09 /* GlucoseStored+CoreDataProperties.swift */; };
 		DDE179522C910127003CDDB7 /* MealPresetStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179322C910127003CDDB7 /* MealPresetStored+CoreDataClass.swift */; };
 		DDE179532C910127003CDDB7 /* MealPresetStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179332C910127003CDDB7 /* MealPresetStored+CoreDataProperties.swift */; };
 		DDE179542C910127003CDDB7 /* LoopStatRecord+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179342C910127003CDDB7 /* LoopStatRecord+CoreDataClass.swift */; };
@@ -668,8 +670,6 @@
 		DDE179612C910127003CDDB7 /* StatsData+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179412C910127003CDDB7 /* StatsData+CoreDataProperties.swift */; };
 		DDE179622C910127003CDDB7 /* Forecast+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179422C910127003CDDB7 /* Forecast+CoreDataClass.swift */; };
 		DDE179632C910127003CDDB7 /* Forecast+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179432C910127003CDDB7 /* Forecast+CoreDataProperties.swift */; };
-		DDE179642C910127003CDDB7 /* GlucoseStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179442C910127003CDDB7 /* GlucoseStored+CoreDataClass.swift */; };
-		DDE179652C910127003CDDB7 /* GlucoseStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179452C910127003CDDB7 /* GlucoseStored+CoreDataProperties.swift */; };
 		DDE179662C910127003CDDB7 /* OpenAPS_Battery+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179462C910127003CDDB7 /* OpenAPS_Battery+CoreDataClass.swift */; };
 		DDE179672C910127003CDDB7 /* OpenAPS_Battery+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179472C910127003CDDB7 /* OpenAPS_Battery+CoreDataProperties.swift */; };
 		DDE179682C910127003CDDB7 /* TempBasalStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179482C910127003CDDB7 /* TempBasalStored+CoreDataClass.swift */; };
@@ -1335,7 +1335,6 @@
 		CE95BF4A2BA5CED700DC3DE3 /* LoopKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = LoopKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		CE95BF562BA5F5FE00DC3DE3 /* PluginManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginManager.swift; sourceTree = "<group>"; };
 		CE95BF592BA62E4A00DC3DE3 /* PluginSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginSource.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>"; };
@@ -1457,6 +1456,7 @@
 		DD9ECB6E2CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigProvider.swift; sourceTree = "<group>"; };
 		DD9ECB6F2CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigDataFlow.swift; sourceTree = "<group>"; };
 		DD9ECB732CA9A0C300AA7C45 /* RemoteControlConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfig.swift; sourceTree = "<group>"; };
+		DDA40BB92F4DB18100257798 /* AlgorithmGlucose.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlgorithmGlucose.swift; sourceTree = "<group>"; };
 		DDA6E24F2D22187500C2988C /* ChartLegendView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartLegendView.swift; sourceTree = "<group>"; };
 		DDA6E2842D2361F800C2988C /* LoopStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopStatusView.swift; sourceTree = "<group>"; };
 		DDA6E31F2D258E0500C2988C /* OverrideHelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideHelpView.swift; sourceTree = "<group>"; };
@@ -1489,6 +1489,8 @@
 		DDD78A902DC4064800AC63F3 /* carbhistory.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = carbhistory.json; sourceTree = "<group>"; };
 		DDD78AD72DC421B500AC63F3 /* enacted.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = enacted.json; sourceTree = "<group>"; };
 		DDD78AD82DC421B500AC63F3 /* suggested.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = suggested.json; sourceTree = "<group>"; };
+		DDD7C8BF2F4DB45400E5CF09 /* GlucoseStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GlucoseStored+CoreDataClass.swift"; sourceTree = "<group>"; };
+		DDD7C8C02F4DB45400E5CF09 /* GlucoseStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GlucoseStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		DDE179322C910127003CDDB7 /* MealPresetStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MealPresetStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		DDE179332C910127003CDDB7 /* MealPresetStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MealPresetStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		DDE179342C910127003CDDB7 /* LoopStatRecord+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LoopStatRecord+CoreDataClass.swift"; sourceTree = "<group>"; };
@@ -1505,8 +1507,6 @@
 		DDE179412C910127003CDDB7 /* StatsData+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatsData+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		DDE179422C910127003CDDB7 /* Forecast+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Forecast+CoreDataClass.swift"; sourceTree = "<group>"; };
 		DDE179432C910127003CDDB7 /* Forecast+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Forecast+CoreDataProperties.swift"; sourceTree = "<group>"; };
-		DDE179442C910127003CDDB7 /* GlucoseStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GlucoseStored+CoreDataClass.swift"; sourceTree = "<group>"; };
-		DDE179452C910127003CDDB7 /* GlucoseStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GlucoseStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		DDE179462C910127003CDDB7 /* OpenAPS_Battery+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OpenAPS_Battery+CoreDataClass.swift"; sourceTree = "<group>"; };
 		DDE179472C910127003CDDB7 /* OpenAPS_Battery+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OpenAPS_Battery+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		DDE179482C910127003CDDB7 /* TempBasalStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempBasalStored+CoreDataClass.swift"; sourceTree = "<group>"; };
@@ -2369,6 +2369,7 @@
 		388E5A5925B6F0250019842D /* Models */ = {
 			isa = PBXGroup;
 			children = (
+				DDA40BB92F4DB18100257798 /* AlgorithmGlucose.swift */,
 				DD3D60302F0377350021A33B /* ExportSetting.swift */,
 				DDFF204F2DB2C11900AB8A96 /* WatchStateSnapshot.swift */,
 				DDEBB05B2D89E9050032305D /* TimeInRangeType.swift */,
@@ -2463,7 +2464,6 @@
 				3811DEE325CA063400A708ED /* PropertyWrappers */,
 				3811DE5525C9D4D500A708ED /* Publisher.swift */,
 				DD6B7CB12C7B6F0800B75029 /* Rounding.swift */,
-				CEA4F62229BE10F70011ADF7 /* SavitzkyGolayFilter.swift */,
 				38E98A3625F5509500C0CED0 /* String+Extensions.swift */,
 				49239B422EEA27AD00469145 /* TempTargetCalculations.swift */,
 				DDCAE8322D78D49C00B1BB51 /* TherapySettingsUtil.swift */,
@@ -3580,6 +3580,8 @@
 		DDE179112C9100FA003CDDB7 /* Classes+Properties */ = {
 			isa = PBXGroup;
 			children = (
+				DDD7C8BF2F4DB45400E5CF09 /* GlucoseStored+CoreDataClass.swift */,
+				DDD7C8C02F4DB45400E5CF09 /* GlucoseStored+CoreDataProperties.swift */,
 				BD4D738B2D15A4080052227B /* TDDStored+CoreDataClass.swift */,
 				BD4D738C2D15A4080052227B /* TDDStored+CoreDataProperties.swift */,
 				DDE179362C910127003CDDB7 /* BolusStored+CoreDataClass.swift */,
@@ -3594,8 +3596,6 @@
 				DDE179432C910127003CDDB7 /* Forecast+CoreDataProperties.swift */,
 				DDE179382C910127003CDDB7 /* ForecastValue+CoreDataClass.swift */,
 				DDE179392C910127003CDDB7 /* ForecastValue+CoreDataProperties.swift */,
-				DDE179442C910127003CDDB7 /* GlucoseStored+CoreDataClass.swift */,
-				DDE179452C910127003CDDB7 /* GlucoseStored+CoreDataProperties.swift */,
 				DDE179342C910127003CDDB7 /* LoopStatRecord+CoreDataClass.swift */,
 				DDE179352C910127003CDDB7 /* LoopStatRecord+CoreDataProperties.swift */,
 				DDE179322C910127003CDDB7 /* MealPresetStored+CoreDataClass.swift */,
@@ -4147,6 +4147,7 @@
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				DDA40BBA2F4DB18800257798 /* AlgorithmGlucose.swift in Sources */,
 				DD5DC9F12CF3D97C00AB8703 /* AdjustmentsStateModel+Overrides.swift in Sources */,
 				3811DE2325C9D48300A708ED /* MainDataFlow.swift in Sources */,
 				C2A0A42F2CE03131003B98E8 /* ConstantValues.swift in Sources */,
@@ -4175,7 +4176,6 @@
 				DD98ACC02D71013200C0778F /* StatChartUtils.swift in Sources */,
 				BD47FDD92D8B657D0043966B /* InsulinSensitivityStepView.swift in Sources */,
 				3862CC2E2743F9F700BF832C /* CalendarManager.swift in Sources */,
-				CEA4F62329BE10F70011ADF7 /* SavitzkyGolayFilter.swift in Sources */,
 				38B4F3C325E2A20B00E76A18 /* PumpSetupView.swift in Sources */,
 				38E4453C274E411700EC9A94 /* Disk+Codable.swift in Sources */,
 				58D08B322C8DF88900AA37D3 /* DummyCharts.swift in Sources */,
@@ -4516,6 +4516,8 @@
 				FA630397F76B582C8D8681A7 /* BasalProfileEditorProvider.swift in Sources */,
 				DD1745172C54389F00211FAC /* FeatureSettingsView.swift in Sources */,
 				DD3D60312F0377350021A33B /* ExportSetting.swift in Sources */,
+				DDD7C8C12F4DB45400E5CF09 /* GlucoseStored+CoreDataClass.swift in Sources */,
+				DDD7C8C22F4DB45400E5CF09 /* GlucoseStored+CoreDataProperties.swift in Sources */,
 				DD9ECB712CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift in Sources */,
 				63E890B4D951EAA91C071D5C /* BasalProfileEditorStateModel.swift in Sources */,
 				38FEF3FA2737E42000574A46 /* BaseStateModel.swift in Sources */,
@@ -4711,8 +4713,6 @@
 				DDE179612C910127003CDDB7 /* StatsData+CoreDataProperties.swift in Sources */,
 				DDE179622C910127003CDDB7 /* Forecast+CoreDataClass.swift in Sources */,
 				DDE179632C910127003CDDB7 /* Forecast+CoreDataProperties.swift in Sources */,
-				DDE179642C910127003CDDB7 /* GlucoseStored+CoreDataClass.swift in Sources */,
-				DDE179652C910127003CDDB7 /* GlucoseStored+CoreDataProperties.swift in Sources */,
 				BDC531142D10611D00088832 /* AddContactImageSheet.swift in Sources */,
 				DDE179662C910127003CDDB7 /* OpenAPS_Battery+CoreDataClass.swift in Sources */,
 				DDE179672C910127003CDDB7 /* OpenAPS_Battery+CoreDataProperties.swift in Sources */,

+ 0 - 12
Trio/Resources/InfoPlist.xcstrings

@@ -457,18 +457,6 @@
         }
       }
     },
-    "NSCalendarsFullAccessUsageDescription" : {
-      "comment" : "Privacy - Calendars Full Access Usage Description",
-      "extractionState" : "extracted_with_value",
-      "localizations" : {
-        "en" : {
-          "stringUnit" : {
-            "state" : "new",
-            "value" : "To create events with BG reading values, so that they can be viewed on Apple Watch and CarPlay"
-          }
-        }
-      }
-    },
     "NSCalendarsUsageDescription" : {
       "comment" : "Privacy - Calendars Usage Description",
       "extractionState" : "extracted_with_value",

+ 7 - 2
Trio/Sources/APS/APSManager.swift

@@ -405,7 +405,7 @@ final class BaseAPSManager: APSManager, Injectable {
         guard let autosense = await storage.retrieveAsync(OpenAPS.Settings.autosense, as: Autosens.self),
               (autosense.timestamp ?? .distantPast).addingTimeInterval(30.minutes.timeInterval) > Date()
         else {
-            let result = try await openAPS.autosense()
+            let result = try await openAPS.autosense(shouldSmoothGlucose: settingsManager.settings.smoothGlucose)
             return result != nil
         }
 
@@ -476,7 +476,11 @@ final class BaseAPSManager: APSManager, Injectable {
 
             _ = try await autosenseResult
             try await openAPS.createProfiles()
-            let determination = try await openAPS.determineBasal(currentTemp: await currentTemp, clock: now)
+            let determination = try await openAPS.determineBasal(
+                currentTemp: await currentTemp,
+                shouldSmoothGlucose: settingsManager.settings.smoothGlucose,
+                clock: now
+            )
             iobFileDidUpdate.send(())
 
             guard isValidGlucoseData else {
@@ -520,6 +524,7 @@ final class BaseAPSManager: APSManager, Injectable {
             let temp = try await fetchCurrentTempBasal(date: Date.now)
             return try await openAPS.determineBasal(
                 currentTemp: temp,
+                shouldSmoothGlucose: settingsManager.settings.smoothGlucose,
                 clock: Date(),
                 simulatedCarbsAmount: simulatedCarbsAmount,
                 simulatedBolusAmount: simulatedBolusAmount,

+ 184 - 39
Trio/Sources/APS/FetchGlucoseManager.swift

@@ -1,4 +1,5 @@
 import Combine
+import CoreData
 import Foundation
 import HealthKit
 import LoopKit
@@ -29,6 +30,7 @@ extension FetchGlucoseManager {
 final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
     private let processQueue = DispatchQueue(label: "BaseGlucoseManager.processQueue")
 
+    @Injected() var broadcaster: Broadcaster!
     @Injected() var glucoseStorage: GlucoseStorage!
     @Injected() var nightscoutManager: NightscoutManager!
     @Injected() var tidepoolService: TidepoolManager!
@@ -66,6 +68,8 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         return cgmManager.shouldSyncToRemoteService
     }
 
+    var shouldSmoothGlucose: Bool = false
+
     init(resolver: Resolver) {
         injectServices(resolver)
         // init at the start of the app
@@ -76,6 +80,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
             cgmGlucoseSourceType: settingsManager.settings.cgm,
             cgmGlucosePluginId: settingsManager.settings.cgmPluginIdentifier
         )
+        shouldSmoothGlucose = settingsManager.settings.smoothGlucose
         subscribe()
     }
 
@@ -117,6 +122,8 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
             .store(in: &lifetime)
         timer.fire()
         timer.resume()
+
+        broadcaster.register(SettingsObserver.self, observer: self)
     }
 
     /// Store new glucose readings from the CGM manager
@@ -197,7 +204,6 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         if let manager = newManager {
             cgmManager = manager
             removeCalibrations()
-//            glucoseSource = nil
         } else if self.cgmGlucoseSourceType == .plugin, cgmManager == nil, let rawCGMManager = rawCGMManager {
             cgmManager = cgmManagerFromRawValue(rawCGMManager)
             updateManagerUnits(cgmManager)
@@ -234,40 +240,17 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         return Manager.init(rawState: rawState)
     }
 
-    private func fetchGlucose() async throws -> [GlucoseStored]? {
+    func fetchGlucose() async throws -> [GlucoseStored]? {
         try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             onContext: context,
-            predicate: NSPredicate.predicateFor30MinAgo,
+            predicate: NSPredicate.predicateForOneDayAgoInMinutes,
             key: "date",
             ascending: false,
-            fetchLimit: 6
+            fetchLimit: 350
         ) as? [GlucoseStored]
     }
 
-    private func processGlucose() async throws -> [BloodGlucose] {
-        let results = try await fetchGlucose()
-
-        return try await context.perform {
-            guard let results else {
-                throw CoreDataError.fetchError(function: #function, file: #file)
-            }
-            return results.map { result in
-                BloodGlucose(
-                    sgv: Int(result.glucose),
-                    direction: BloodGlucose.Direction(from: result.direction ?? ""),
-                    date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
-                    dateString: result.date ?? Date(),
-                    unfiltered: Decimal(result.glucose),
-                    filtered: Decimal(result.glucose),
-                    noise: nil,
-                    glucose: Int(result.glucose),
-                    type: "sgv"
-                )
-            }
-        }
-    }
-
     private func glucoseStoreAndHeartDecision(syncDate: Date, glucose: [BloodGlucose]) async throws {
         // calibration add if required only for sensor
         let newGlucose = overcalibrate(entries: glucose)
@@ -303,21 +286,12 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         }
         debug(.deviceManager, "New glucose found")
 
-        // filter the data if it is the case
-        if settingsManager.settings.smoothGlucose {
-            // limited to 30 min of old glucose data
-            let oldGlucoseValues = try await processGlucose()
+        try await glucoseStorage.storeGlucose(filtered)
 
-            var smoothedValues = oldGlucoseValues + filtered
-            // smooth with 3 repeats
-            for _ in 1 ... 3 {
-                smoothedValues.smoothSavitzkyGolayQuaDratic(withFilterWidth: 3)
-            }
-            // find the new values only
-            filtered = smoothedValues.filter { $0.dateString > syncDate }
+        if settingsManager.settings.smoothGlucose {
+            await exponentialSmoothingGlucose(context: context)
         }
 
-        try await glucoseStorage.storeGlucose(filtered)
         deviceDataManager.heartbeat(date: Date())
 
         endBackgroundTaskSafely(&backgroundTaskID, taskName: "Glucose Store and Heartbeat Decision")
@@ -377,3 +351,174 @@ extension CGMManager {
         ]
     }
 }
+
+extension BaseFetchGlucoseManager: SettingsObserver {
+    /// Smooth glucose data when smoothing is turned on
+    func settingsDidChange(_: TrioSettings) {
+        if settingsManager.settings.smoothGlucose, !shouldSmoothGlucose {
+            glucoseStoreAndHeartLock.wait()
+            Task {
+                await self.exponentialSmoothingGlucose(context: context)
+                glucoseStoreAndHeartLock.signal()
+            }
+        }
+        shouldSmoothGlucose = settingsManager.settings.smoothGlucose
+    }
+}
+
+extension BaseFetchGlucoseManager {
+    /// CoreData-friendly AAPS exponential smoothing + storage.
+    /// - Important: Only stores `smoothedGlucose`. UI/alerts should still use `glucose`.
+    func exponentialSmoothingGlucose(context: NSManagedObjectContext) async {
+        let startTime = Date()
+
+        guard let glucoseStored = try? await fetchGlucose() else { return }
+
+        await context.perform {
+            // Only smooth CGM values; ignore manually entered glucose
+            // Keep only entries with dates
+            let cgmValuesNewestFirst: [GlucoseStored] = glucoseStored
+                .filter { !$0.isManual }
+                .compactMap { obj -> GlucoseStored? in
+                    guard obj.date != nil else { return nil }
+                    return obj
+                }
+                .sorted { $0.date! > $1.date! } // newest first (AAPS expectation)
+
+            guard !cgmValuesNewestFirst.isEmpty else { return }
+
+            // Build a smoothing window size per AAPS rules (gap/xDrip error), then compute smoothed values for
+            // the most recent `limit` entries. Older values are left unchanged (same as the Kotlin behavior).
+            self.applyExponentialSmoothingAndStore(
+                newestFirst: cgmValuesNewestFirst,
+                minimumWindowSize: 4,
+                maximumAllowedGapMinutes: 12,
+                xDripErrorGlucose: 38,
+                minimumSmoothedGlucose: 39,
+                firstOrderWeight: 0.4,
+                firstOrderAlpha: 0.5,
+                secondOrderAlpha: 0.4,
+                secondOrderBeta: 1.0
+            )
+
+            do {
+                try context.save()
+            } catch {
+                // Replace with your logging system if you have one
+                debugPrint("Failed to save context after smoothing: \(error)")
+            }
+        }
+
+        let duration = Date().timeIntervalSince(startTime)
+        debugPrint(String(format: "Exponential smoothing duration: %0.04fs", duration))
+    }
+
+    private func applyExponentialSmoothingAndStore(
+        newestFirst data: [GlucoseStored],
+        minimumWindowSize: Int,
+        maximumAllowedGapMinutes: Int,
+        xDripErrorGlucose: Int,
+        minimumSmoothedGlucose: Decimal,
+        firstOrderWeight: Decimal,
+        firstOrderAlpha: Decimal,
+        secondOrderAlpha: Decimal,
+        secondOrderBeta: Decimal
+    ) {
+        let recordCount = data.count
+        guard recordCount > 0 else { return }
+
+        // We need i+1 access while scanning gaps -> initial validWindowCount must be <= count-1
+        var validWindowCount = max(recordCount - 1, 0)
+
+        // Trim window based on rounded minute gaps or xDrip error value (38)
+        if validWindowCount > 0 {
+            for i in 0 ..< validWindowCount {
+                guard let newerDate = data[i].date, let olderDate = data[i + 1].date else { continue }
+
+                let gapSeconds = newerDate.timeIntervalSince(olderDate)
+                let gapMinutesRounded = Int((gapSeconds / 60.0).rounded()) // Kotlin: round(...)
+
+                if gapMinutesRounded >= maximumAllowedGapMinutes {
+                    validWindowCount = i + 1 // include the more recent reading
+                    break
+                }
+
+                if Int(data[i].glucose) == xDripErrorGlucose {
+                    validWindowCount = i // exclude this 38 value
+                    break
+                }
+            }
+        }
+
+        // If insufficient valid readings: copy raw into smoothed (clamped) for all passed entries
+        guard validWindowCount >= minimumWindowSize else {
+            for obj in data {
+                let raw = Decimal(Int(obj.glucose))
+                obj.smoothedGlucose = max(raw, minimumSmoothedGlucose) as NSDecimalNumber
+                obj.direction = .none
+            }
+            return
+        }
+
+        // ---- 1st order smoothing (newest-first arrays, Kotlin add(0, ...) equivalent) ----
+        var firstOrderSmoothed: [Decimal] = []
+        firstOrderSmoothed.reserveCapacity(validWindowCount + 1)
+
+        // Initialize with the oldest valid point (index validWindowCount - 1)
+        firstOrderSmoothed = [Decimal(Int(data[validWindowCount - 1].glucose))]
+
+        for i in 0 ..< validWindowCount {
+            let raw = Decimal(Int(data[validWindowCount - 1 - i].glucose))
+            let prev = firstOrderSmoothed[0]
+            let next = prev + firstOrderAlpha * (raw - prev)
+            firstOrderSmoothed.insert(next, at: 0)
+        }
+
+        // ---- 2nd order smoothing ----
+        var secondOrderSmoothed: [Decimal] = []
+        var secondOrderDelta: [Decimal] = []
+        secondOrderSmoothed.reserveCapacity(validWindowCount)
+        secondOrderDelta.reserveCapacity(validWindowCount)
+
+        secondOrderSmoothed = [Decimal(Int(data[validWindowCount - 1].glucose))]
+        secondOrderDelta = [
+            Decimal(Int(data[validWindowCount - 2].glucose) - Int(data[validWindowCount - 1].glucose))
+        ]
+
+        for i in 0 ..< (validWindowCount - 1) {
+            let raw = Decimal(Int(data[validWindowCount - 2 - i].glucose))
+
+            let sBG = secondOrderSmoothed[0]
+            let sD = secondOrderDelta[0]
+
+            let nextBG = secondOrderAlpha * raw + (1 - secondOrderAlpha) * (sBG + sD)
+            secondOrderSmoothed.insert(nextBG, at: 0)
+
+            let nextD =
+                secondOrderBeta * (secondOrderSmoothed[0] - secondOrderSmoothed[1])
+                    + (1 - secondOrderBeta) * secondOrderDelta[0]
+            secondOrderDelta.insert(nextD, at: 0)
+        }
+
+        // ---- Weighted blend ----
+        var blended: [Decimal] = []
+        blended.reserveCapacity(secondOrderSmoothed.count)
+
+        for i in secondOrderSmoothed.indices {
+            let value =
+                firstOrderWeight * firstOrderSmoothed[i]
+                    + (1 - firstOrderWeight) * secondOrderSmoothed[i]
+            blended.append(value)
+        }
+
+        // Apply to the most recent `limit` readings (same behavior as Kotlin)
+        let limit = min(blended.count, data.count)
+        for i in 0 ..< limit {
+            let rounded = blended[i].rounded(toPlaces: 0) // nearest integer, ties away from zero
+            let clamped = max(rounded, minimumSmoothedGlucose)
+
+            data[i].smoothedGlucose = clamped as NSDecimalNumber
+            data[i].direction = .none
+        }
+    }
+}

+ 47 - 20
Trio/Sources/APS/OpenAPS/OpenAPS.swift

@@ -98,26 +98,52 @@ final class OpenAPS {
     }
 
     // fetch glucose to pass it to the meal function and to determine basal
-    private func fetchAndProcessGlucose(fetchLimit: Int?) async throws -> String {
-        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: GlucoseStored.self,
-            onContext: context,
-            predicate: NSPredicate.predicateForOneDayAgoInMinutes,
-            key: "date",
-            ascending: false,
-            fetchLimit: fetchLimit,
-            batchSize: 48
-        )
+    func fetchAndProcessGlucose(
+            context: NSManagedObjectContext,
+            shouldSmoothGlucose: Bool,
+            fetchLimit: Int?
+        ) async throws -> String {
+            let results = try CoreDataStack.shared.fetchEntities(
+                ofType: GlucoseStored.self,
+                onContext: context,
+                predicate: NSPredicate.predicateForOneDayAgoInMinutes,
+                key: "date",
+                ascending: false,
+                fetchLimit: fetchLimit,
+                batchSize: 48
+            )
 
-        return try await context.perform {
-            guard let glucoseResults = results as? [GlucoseStored] else {
-                throw CoreDataError.fetchError(function: #function, file: #file)
-            }
+            return try await context.perform {
+                guard let glucoseResults = results as? [GlucoseStored] else {
+                    throw CoreDataError.fetchError(function: #function, file: #file)
+                }
 
-            // convert to JSON
-            return self.jsonConverter.convertToJSON(glucoseResults)
+                let algorithmGlucose = glucoseResults.map { glucose -> AlgorithmGlucose in
+                    let glucoseValue: Int16
+                    if shouldSmoothGlucose, !glucose.isManual, let smoothedGlucose = glucose.smoothedGlucose {
+                        let roundingBehavior = NSDecimalNumberHandler(
+                            roundingMode: .plain,
+                            scale: 0,
+                            raiseOnExactness: false,
+                            raiseOnOverflow: false,
+                            raiseOnUnderflow: false,
+                            raiseOnDivideByZero: false
+                        )
+                        glucoseValue = smoothedGlucose.rounding(accordingToBehavior: roundingBehavior).int16Value
+                    } else {
+                        glucoseValue = glucose.glucose
+                    }
+                    return AlgorithmGlucose(
+                        date: glucose.date,
+                        direction: glucose.direction,
+                        glucose: glucoseValue,
+                        id: glucose.id,
+                        isManual: glucose.isManual
+                    )
+                }
+                return self.jsonConverter.convertToJSON(algorithmGlucose)
+            }
         }
-    }
 
     private func fetchAndProcessCarbs(additionalCarbs: Decimal? = nil, carbsDate: Date? = nil) async throws -> String {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
@@ -278,6 +304,7 @@ final class OpenAPS {
 
     func determineBasal(
         currentTemp: TempBasal,
+        shouldSmoothGlucose: Bool,
         clock: Date = Date(),
         simulatedCarbsAmount: Decimal? = nil,
         simulatedBolusAmount: Decimal? = nil,
@@ -292,7 +319,7 @@ final class OpenAPS {
         // Perform asynchronous calls in parallel
         async let pumpHistoryObjectIDs = fetchPumpHistoryObjectIDs() ?? []
         async let carbs = fetchAndProcessCarbs(additionalCarbs: simulatedCarbsAmount ?? 0, carbsDate: simulatedCarbsDate)
-        async let glucose = fetchAndProcessGlucose(fetchLimit: 72)
+        async let glucose = fetchAndProcessGlucose(context: context, shouldSmoothGlucose: shouldSmoothGlucose, fetchLimit: 72)
         async let prepareTrioCustomOrefVariables = prepareTrioCustomOrefVariables()
         async let profileAsync = loadFileFromStorageAsync(name: Settings.profile)
         async let basalAsync = loadFileFromStorageAsync(name: Settings.basalProfile)
@@ -461,13 +488,13 @@ final class OpenAPS {
         }
     }
 
-    func autosense() async throws -> Autosens? {
+    func autosense(shouldSmoothGlucose: Bool) async throws -> Autosens? {
         debug(.openAPS, "Start autosens")
 
         // Perform asynchronous calls in parallel
         async let pumpHistoryObjectIDs = fetchPumpHistoryObjectIDs() ?? []
         async let carbs = fetchAndProcessCarbs()
-        async let glucose = fetchAndProcessGlucose(fetchLimit: nil)
+        async let glucose = fetchAndProcessGlucose(context: context, shouldSmoothGlucose: shouldSmoothGlucose, fetchLimit: nil)
         async let getProfile = loadFileFromStorageAsync(name: Settings.profile)
         async let getBasalProfile = loadFileFromStorageAsync(name: Settings.basalProfile)
         async let getTempTargets = loadFileFromStorageAsync(name: Settings.tempTargets)

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

@@ -1,172 +0,0 @@
-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 }
-}

+ 1 - 0
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -10179,6 +10179,7 @@
       }
     },
     "%lld h" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {

+ 84 - 0
Trio/Sources/Models/AlgorithmGlucose.swift

@@ -0,0 +1,84 @@
+/// Helper class so that we can have a plain Swift object to serialize GlucoseStorage
+struct AlgorithmGlucose: Codable {
+    var date: Date?
+    var direction: String?
+    var glucose: Int16
+    var id: UUID?
+    var isManual: Bool
+
+    enum CodingKeys: String, CodingKey {
+        case date
+        case dateString
+        case sgv
+        case glucose
+        case direction
+        case id
+        case type
+    }
+
+    init(date: Date?, direction: String?, glucose: Int16, id: UUID?, isManual: Bool) {
+        self.date = date
+        self.direction = direction
+        self.glucose = glucose
+        self.id = id
+        self.isManual = isManual
+    }
+
+    // this constructor is just for testing
+    public init(from decoder: Decoder) throws {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+
+        if let dateString = try container.decodeIfPresent(String.self, forKey: .dateString) {
+            let dateFormatter = ISO8601DateFormatter()
+            dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
+            date = dateFormatter.date(from: dateString)
+        } else if let dateStringTimestamp = try container.decodeIfPresent(String.self, forKey: .date),
+                  let dateTimestamp = TimeInterval(dateStringTimestamp)
+        {
+            date = Date(timeIntervalSince1970: dateTimestamp / 1000)
+        } else {
+            date = nil
+        }
+
+        direction = try container.decodeIfPresent(String.self, forKey: .direction)
+        id = try container.decodeIfPresent(UUID.self, forKey: .id)
+
+        if let glucoseValue = try container.decodeIfPresent(Int16.self, forKey: .glucose) {
+            glucose = glucoseValue
+            isManual = true
+        } else if let sgvValue = try container.decodeIfPresent(Int16.self, forKey: .sgv) {
+            glucose = sgvValue
+            isManual = false
+        } else {
+            throw DecodingError.dataCorruptedError(
+                forKey: .sgv,
+                in: container,
+                debugDescription: "Neither 'glucose' nor 'sgv' key found or value is not Int16"
+            )
+        }
+    }
+
+    public func encode(to encoder: Encoder) throws {
+        var container = encoder.container(keyedBy: CodingKeys.self)
+
+        let dateFormatter = ISO8601DateFormatter()
+        dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
+
+        try container.encode(dateFormatter.string(from: date ?? Date()), forKey: .dateString)
+
+        let dateAsUnixTimestamp = String(format: "%.0f", (date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000)
+        try container.encode(dateAsUnixTimestamp, forKey: .date)
+
+        try container.encode(direction, forKey: .direction)
+        try container.encode(id, forKey: .id)
+
+        // TODO: Handle the type of the glucose entry conditionally not hardcoded
+        try container.encode("sgv", forKey: .type)
+
+        if isManual {
+            try container.encode(glucose, forKey: .glucose)
+        } else {
+            try container.encode(glucose, forKey: .sgv)
+        }
+    }
+}

+ 0 - 12
Trio/Sources/Models/BloodGlucose.swift

@@ -249,18 +249,6 @@ extension NumberFormatter {
     }()
 }
 
-extension BloodGlucose: SavitzkyGolaySmoothable {
-    var value: Double {
-        get {
-            Double(glucose ?? 0)
-        }
-        set {
-            glucose = Int(newValue)
-            sgv = Int(newValue)
-        }
-    }
-}
-
 extension BloodGlucose {
     func convertStoredGlucoseSample(isManualGlucose: Bool) -> StoredGlucoseSample {
         StoredGlucoseSample(