Jelajahi Sumber

Merge branch 'bdb_dev' into Crowdin

Jon B.M 3 tahun lalu
induk
melakukan
e2396e1288
31 mengubah file dengan 620 tambahan dan 76 penghapusan
  1. 41 9
      FreeAPS.xcodeproj/project.pbxproj
  2. 2 2
      FreeAPS/Resources/Base.lproj/InfoPlist.strings
  3. 3 3
      FreeAPS/Resources/Info.plist
  4. 2 2
      FreeAPS/Resources/ar.lproj/InfoPlist.strings
  5. 6 1
      FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json
  6. 2 2
      FreeAPS/Resources/sv.lproj/InfoPlist.strings
  7. 10 2
      FreeAPS/Sources/APS/APSManager.swift
  8. 1 1
      FreeAPS/Sources/APS/CGM/DexcomSourceG6.swift
  9. 1 3
      FreeAPS/Sources/APS/DeviceDataManager.swift
  10. 33 1
      FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings
  11. 6 0
      FreeAPS/Sources/Localizations/Main/sv.lproj/Localizable.strings
  12. 3 0
      FreeAPS/Sources/Models/CarbsEntry.swift
  13. 25 0
      FreeAPS/Sources/Models/FreeAPSSettings.swift
  14. 42 4
      FreeAPS/Sources/Modules/AddCarbs/AddCarbsStateModel.swift
  15. 38 8
      FreeAPS/Sources/Modules/AddCarbs/View/AddCarbsRootView.swift
  16. 9 4
      FreeAPS/Sources/Modules/DataTable/DataTableDataFlow.swift
  17. 9 5
      FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift
  18. 16 6
      FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift
  19. 2 2
      FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift
  20. 5 0
      FreeAPS/Sources/Modules/FPUConfig/FPUConfigDataFlow.swift
  21. 3 0
      FreeAPS/Sources/Modules/FPUConfig/FPUConfigProvider.swift
  22. 44 0
      FreeAPS/Sources/Modules/FPUConfig/FPUConfigStateModel.swift
  23. 64 0
      FreeAPS/Sources/Modules/FPUConfig/View/FPUConfigRootView.swift
  24. 1 1
      FreeAPS/Sources/Modules/HealthKit/HealthKitStateModel.swift
  25. 1 1
      FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift
  26. 0 3
      FreeAPS/Sources/Modules/Settings/SettingsStateModel.swift
  27. 1 0
      FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift
  28. 3 0
      FreeAPS/Sources/Router/Screen.swift
  29. 238 15
      FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift
  30. 1 1
      FreeAPS/Sources/Services/WatchManager/WatchManager.swift
  31. 8 0
      fastlane/testflight.md

+ 41 - 9
FreeAPS.xcodeproj/project.pbxproj

@@ -21,6 +21,10 @@
 		19854F492961C3E500941627 /* DurationButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19854F482961C3E500941627 /* DurationButton.swift */; };
 		199561C1275E61A50077B976 /* HealthKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 199561C0275E61A50077B976 /* HealthKit.framework */; };
 		19B0EF2128F6D66200069496 /* Statistics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19B0EF2028F6D66200069496 /* Statistics.swift */; };
+		19D466A329AA2B80004D5F33 /* FPUConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19D466A229AA2B80004D5F33 /* FPUConfigDataFlow.swift */; };
+		19D466A529AA2BD4004D5F33 /* FPUConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19D466A429AA2BD4004D5F33 /* FPUConfigProvider.swift */; };
+		19D466A729AA2C22004D5F33 /* FPUConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19D466A629AA2C22004D5F33 /* FPUConfigStateModel.swift */; };
+		19D466AA29AA3099004D5F33 /* FPUConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19D466A929AA3099004D5F33 /* FPUConfigRootView.swift */; };
 		1BBB001DAD60F3B8CEA4B1C7 /* ISFEditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505E09DC17A0C3D0AF4B66FE /* ISFEditorStateModel.swift */; };
 		1D845DF2E3324130E1D95E67 /* DataTableProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60744C3E9BB3652895C908CC /* DataTableProvider.swift */; };
 		23888883D4EA091C88480FF2 /* BolusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C19984D62EFC0035A9E9644D /* BolusProvider.swift */; };
@@ -487,6 +491,10 @@
 		19B0EF2028F6D66200069496 /* Statistics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Statistics.swift; sourceTree = "<group>"; };
 		19C166682756EFBD00ED12E3 /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/InfoPlist.strings; sourceTree = "<group>"; };
 		19C166692756EFBD00ED12E3 /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Localizable.strings; sourceTree = "<group>"; };
+		19D466A229AA2B80004D5F33 /* FPUConfigDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FPUConfigDataFlow.swift; sourceTree = "<group>"; };
+		19D466A429AA2BD4004D5F33 /* FPUConfigProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FPUConfigProvider.swift; sourceTree = "<group>"; };
+		19D466A629AA2C22004D5F33 /* FPUConfigStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FPUConfigStateModel.swift; sourceTree = "<group>"; };
+		19D466A929AA3099004D5F33 /* FPUConfigRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FPUConfigRootView.swift; sourceTree = "<group>"; };
 		1CAE81192B118804DCD23034 /* SnoozeProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SnoozeProvider.swift; sourceTree = "<group>"; };
 		212E8BFE6D66EE65AA26A114 /* CalibrationsProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CalibrationsProvider.swift; sourceTree = "<group>"; };
 		223EC0494F55A91E3EA69EF4 /* BolusStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BolusStateModel.swift; sourceTree = "<group>"; };
@@ -927,6 +935,25 @@
 			path = Main;
 			sourceTree = "<group>";
 		};
+		19D466A129AA2B0A004D5F33 /* FPUConfig */ = {
+			isa = PBXGroup;
+			children = (
+				19D466A229AA2B80004D5F33 /* FPUConfigDataFlow.swift */,
+				19D466A429AA2BD4004D5F33 /* FPUConfigProvider.swift */,
+				19D466A629AA2C22004D5F33 /* FPUConfigStateModel.swift */,
+				19D466A829AA306E004D5F33 /* View */,
+			);
+			path = FPUConfig;
+			sourceTree = "<group>";
+		};
+		19D466A829AA306E004D5F33 /* View */ = {
+			isa = PBXGroup;
+			children = (
+				19D466A929AA3099004D5F33 /* FPUConfigRootView.swift */,
+			);
+			path = View;
+			sourceTree = "<group>";
+		};
 		29B478DF61BF8D270F7D8954 /* Snooze */ = {
 			isa = PBXGroup;
 			children = (
@@ -949,6 +976,7 @@
 		3811DE0325C9D31700A708ED /* Modules */ = {
 			isa = PBXGroup;
 			children = (
+				19D466A129AA2B0A004D5F33 /* FPUConfig */,
 				F90692CD274B99850037068D /* HealthKit */,
 				6DC5D590658EF8B8DF94F9F5 /* AddCarbs */,
 				A9A4C88374496B3C89058A89 /* AddTempTarget */,
@@ -2233,6 +2261,7 @@
 				38E44537274E411700EC9A94 /* Disk+Helpers.swift in Sources */,
 				388E5A6025B6F2310019842D /* Autosens.swift in Sources */,
 				3811DE8F25C9D80400A708ED /* User.swift in Sources */,
+				19D466A329AA2B80004D5F33 /* FPUConfigDataFlow.swift in Sources */,
 				3811DEB225C9D88300A708ED /* KeychainItemAccessibility.swift in Sources */,
 				385CEAC425F2F154002D6D5B /* AnnouncementsStorage.swift in Sources */,
 				38AEE73D25F0200C0013F05B /* FreeAPSSettings.swift in Sources */,
@@ -2271,6 +2300,7 @@
 				38DAB280260CBB7F00F74C1A /* PumpView.swift in Sources */,
 				3811DEB125C9D88300A708ED /* Keychain.swift in Sources */,
 				382C133725F13A1E00715CE1 /* InsulinSensitivities.swift in Sources */,
+				19D466A529AA2BD4004D5F33 /* FPUConfigProvider.swift in Sources */,
 				383948D625CD4D8900E91849 /* FileStorage.swift in Sources */,
 				3811DE4125C9D4A100A708ED /* SettingsRootView.swift in Sources */,
 				38192E04261B82FA0094D973 /* ReachabilityManager.swift in Sources */,
@@ -2289,6 +2319,7 @@
 				3811DF0225CA9FEA00A708ED /* Credentials.swift in Sources */,
 				389A572026079BAA00BC102F /* Interpolation.swift in Sources */,
 				38B4F3C625E5017E00E76A18 /* NotificationCenter.swift in Sources */,
+				19D466A729AA2C22004D5F33 /* FPUConfigStateModel.swift in Sources */,
 				38E44528274E401C00EC9A94 /* Protected.swift in Sources */,
 				3811DEB625C9D88300A708ED /* UnlockManager.swift in Sources */,
 				E00EEC0827368630002FF094 /* NetworkAssembly.swift in Sources */,
@@ -2375,6 +2406,7 @@
 				AD3D2CD42CD01B9EB8F26522 /* PumpConfigDataFlow.swift in Sources */,
 				53F2382465BF74DB1A967C8B /* PumpConfigProvider.swift in Sources */,
 				5D16287A969E64D18CE40E44 /* PumpConfigStateModel.swift in Sources */,
+				19D466AA29AA3099004D5F33 /* FPUConfigRootView.swift in Sources */,
 				E974172296125A5AE99E634C /* PumpConfigRootView.swift in Sources */,
 				448B6FCB252BD4796E2960C0 /* PumpSettingsEditorDataFlow.swift in Sources */,
 				38E44536274E411700EC9A94 /* Disk.swift in Sources */,
@@ -2732,7 +2764,7 @@
 			buildSettings = {
 				APP_DISPLAY_NAME = "$(APP_DISPLAY_NAME)";
 				APP_GROUP_ID = "$(APP_GROUP_ID)";
-				ASSETCATALOG_COMPILER_APPICON_NAME = "$(APP_ICON)";
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon_BW;
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER)";
 				CODE_SIGN_ENTITLEMENTS = FreeAPS/Resources/FreeAPS.entitlements;
@@ -2768,7 +2800,7 @@
 			buildSettings = {
 				APP_DISPLAY_NAME = "$(APP_DISPLAY_NAME)";
 				APP_GROUP_ID = "$(APP_GROUP_ID)";
-				ASSETCATALOG_COMPILER_APPICON_NAME = "$(APP_ICON)";
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon_BW;
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER)";
 				CODE_SIGN_ENTITLEMENTS = FreeAPS/Resources/FreeAPS.entitlements;
@@ -2804,7 +2836,7 @@
 			buildSettings = {
 				ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
 				APP_DISPLAY_NAME = "$(APP_DISPLAY_NAME)";
-				ASSETCATALOG_COMPILER_APPICON_NAME = "$(APP_ICON)";
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon_BW;
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER)";
 				CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
@@ -2834,7 +2866,7 @@
 			buildSettings = {
 				ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
 				APP_DISPLAY_NAME = "$(APP_DISPLAY_NAME)";
-				ASSETCATALOG_COMPILER_APPICON_NAME = "$(APP_ICON)";
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon_BW;
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER)";
 				CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
@@ -2991,7 +3023,7 @@
 				388E596625AD948E0019842D /* Release */,
 			);
 			defaultConfigurationIsVisible = 0;
-			defaultConfigurationName = Release;
+			defaultConfigurationName = Debug;
 		};
 		388E596725AD948E0019842D /* Build configuration list for PBXNativeTarget "FreeAPS" */ = {
 			isa = XCConfigurationList;
@@ -3000,7 +3032,7 @@
 				388E596925AD948E0019842D /* Release */,
 			);
 			defaultConfigurationIsVisible = 0;
-			defaultConfigurationName = Release;
+			defaultConfigurationName = Debug;
 		};
 		38E8754327554D5900975559 /* Build configuration list for PBXNativeTarget "FreeAPSWatch WatchKit Extension" */ = {
 			isa = XCConfigurationList;
@@ -3009,7 +3041,7 @@
 				38E8754227554D5900975559 /* Release */,
 			);
 			defaultConfigurationIsVisible = 0;
-			defaultConfigurationName = Release;
+			defaultConfigurationName = Debug;
 		};
 		38E8754427554D5900975559 /* Build configuration list for PBXNativeTarget "FreeAPSWatch" */ = {
 			isa = XCConfigurationList;
@@ -3018,7 +3050,7 @@
 				38E8753F27554D5900975559 /* Release */,
 			);
 			defaultConfigurationIsVisible = 0;
-			defaultConfigurationName = Release;
+			defaultConfigurationName = Debug;
 		};
 		38FCF3F425E9028E0078B0D1 /* Build configuration list for PBXNativeTarget "FreeAPSTests" */ = {
 			isa = XCConfigurationList;
@@ -3027,7 +3059,7 @@
 				38FCF3F625E9028E0078B0D1 /* Release */,
 			);
 			defaultConfigurationIsVisible = 0;
-			defaultConfigurationName = Release;
+			defaultConfigurationName = Debug;
 		};
 /* End XCConfigurationList section */
 

+ 2 - 2
FreeAPS/Resources/Base.lproj/InfoPlist.strings

@@ -14,7 +14,7 @@
 "NSCalendarsUsageDescription" = "Calendar is used to create a new glucose events.";
 
 /* Privacy - Health Update Usage Description */
-"NSHealthUpdateUsageDescription" = "Health App is used to store blood glucose data";
+"NSHealthUpdateUsageDescription" = "Health App is used to store blood glucose, insulin and carbohydrates";
 
 /* Privacy - Health Share Usage Description */
-"NSHealthShareUsageDescription" = "Health App is used to store blood glucose data";
+"NSHealthShareUsageDescription" = "Health App is used to store blood glucose, insulin and carbohydrates";

+ 3 - 3
FreeAPS/Resources/Info.plist

@@ -71,11 +71,11 @@
 	<key>NSFaceIDUsageDescription</key>
 	<string>For authorized acces to bolus</string>
 	<key>NSHealthShareUsageDescription</key>
-	<string>Health App is used to store blood glucose data</string>
+	<string>Health App is used to store blood glucose, carbs and insulin</string>
 	<key>NSHealthUpdateUsageDescription</key>
-	<string>Health App is used to store blood glucose data</string>
+	<string>Health App is used to store blood glucose, carbs and insulin</string>
 	<key>NSHumanReadableCopyright</key>
-	<string>$(BRANCH)</string>
+	<string>$(COPYRIGHT_NOTICE)</string>
 	<key>UIApplicationSceneManifest</key>
 	<dict>
 		<key>UIApplicationSupportsMultipleScenes</key>

+ 2 - 2
FreeAPS/Resources/ar.lproj/InfoPlist.strings

@@ -14,7 +14,7 @@
 "NSCalendarsUsageDescription" = "Calendar is used to create a new glucose events.";
 
 /* Privacy - Health Update Usage Description */
-"NSHealthUpdateUsageDescription" = "Health App is used to store blood glucose data";
+"NSHealthUpdateUsageDescription" = "Health App is used to store blood glucose, carbs and insulin";
 
 /* Privacy - Health Share Usage Description */
-"NSHealthShareUsageDescription" = "Health App is used to store blood glucose data";
+"NSHealthShareUsageDescription" = "Health App is used to store blood glucose, carbs and insulin";

+ 6 - 1
FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json

@@ -21,5 +21,10 @@
     "carbsRequiredThreshold": 10,
     "useAppleHealth": false,
     "animatedBackground": false,
-    "displayStatistics": false
+    "displayStatistics": false,
+    "useFPUconversion": false
+    "individualAdjustmentFactor": 0.5,
+    "timeCap": 8,
+    "minuteInterval": 60,
+    "delay": 60
 }

+ 2 - 2
FreeAPS/Resources/sv.lproj/InfoPlist.strings

@@ -14,7 +14,7 @@
 "NSCalendarsUsageDescription" = "Kalendern används för att skapa kalenderhändelser för glukosvärden.";
 
 /* Privacy - Health Update Usage Description */
-"NSHealthUpdateUsageDescription" = "Appen Hälsa används för att lagra blodsockervärden etc.";
+"NSHealthUpdateUsageDescription" = "Appen Hälsa används för att lagra blodsockervärden, insulin och kolhydrater.";
 
 /* Privacy - Health Share Usage Description */
-"NSHealthShareUsageDescription" = "Appen Hälsa används för att lagra blodsockervärden etc.";
+"NSHealthShareUsageDescription" = "Appen Hälsa används för att lagra blodsockervärden, insulin och kolhydrater.";

+ 10 - 2
FreeAPS/Sources/APS/APSManager.swift

@@ -72,6 +72,7 @@ final class BaseAPSManager: APSManager, Injectable {
     @Injected() private var nightscout: NightscoutManager!
     @Injected() private var settingsManager: SettingsManager!
     @Injected() private var broadcaster: Broadcaster!
+    @Injected() private var healthKitManager: HealthKitManager!
     @Persisted(key: "lastAutotuneDate") private var lastAutotuneDate = Date()
     @Persisted(key: "lastStartLoopDate") private var lastStartLoopDate: Date = .distantPast
     @Persisted(key: "lastLoopDate") var lastLoopDate: Date = .distantPast {
@@ -250,6 +251,10 @@ final class BaseAPSManager: APSManager, Injectable {
     private func loopCompleted(error: Error? = nil, loopStatRecord: LoopStats) {
         isLooping.send(false)
 
+        // save AH events
+        let events = pumpHistoryStorage.recent()
+        healthKitManager.saveIfNeeded(pumpEvents: events)
+
         if let error = error {
             warning(.apsManager, "Loop failed with error: \(error.localizedDescription)")
             if let backgroundTask = backGroundTaskID {
@@ -895,6 +900,7 @@ final class BaseAPSManager: APSManager, Injectable {
         var timeIntervalLoopArray: [Double] = []
         var medianInterval = 0.0
         var averageIntervalLoops = 0.0
+        var averageLoopDuration = 0.0
 
         coredataContext.performAndWait {
             let requestLSR = LoopStatRecord.fetchRequest() as NSFetchRequest<LoopStatRecord>
@@ -956,6 +962,8 @@ final class BaseAPSManager: APSManager, Injectable {
                 medianInterval = medianCalculation(array: timeIntervalLoopArray)
                 // Average time interval between loops
                 averageIntervalLoops = timeIntervalLoopArray.reduce(0, +) / Double(timeIntervalLoopArray.count)
+                // Average loop duration
+                averageLoopDuration = timeForOneLoopArray.reduce(0, +) / Double(timeForOneLoopArray.count)
             }
         }
 
@@ -1217,11 +1225,11 @@ final class BaseAPSManager: APSManager, Injectable {
             errors: errorNR,
             readings: Int(nrOfCGMReadings),
             success_rate: Decimal(round(successRate ?? 0)),
-            avg_interval: roundDecimal(Decimal(averageIntervalLoops), 1),
+            avg_interval: roundDecimal(Decimal(averageLoopTime), 1),
             median_interval: roundDecimal(Decimal(medianInterval), 1),
             min_interval: roundDecimal(Decimal(minimumInt), 1),
             max_interval: roundDecimal(Decimal(maximumInt), 1),
-            avg_duration: Decimal(roundDouble(averageLoopTime, 2)),
+            avg_duration: Decimal(roundDouble(averageLoopDuration, 2)),
             median_duration: Decimal(roundDouble(medianLoopTime, 2)),
             min_duration: roundDecimal(Decimal(minimumLoopTime), 2),
             max_duration: Decimal(roundDouble(maximumLoopTime, 1))

+ 1 - 1
FreeAPS/Sources/APS/CGM/DexcomSourceG6.swift

@@ -96,7 +96,7 @@ extension DexcomSourceG6: CGMManagerDelegate {
     func recordRetractedAlert(_: LoopKit.Alert, at _: Date) {}
 
     func cgmManagerWantsDeletion(_ manager: CGMManager) {
-        dispatchPrecondition(condition: .onQueue(.main))
+        dispatchPrecondition(condition: .onQueue(processQueue))
         debug(.deviceManager, " CGM Manager with identifier \(manager.managerIdentifier) wants deletion")
         glucoseManager?.cgmGlucoseSourceType = nil
     }

+ 1 - 3
FreeAPS/Sources/APS/DeviceDataManager.swift

@@ -85,13 +85,12 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
             if let pumpManager = pumpManager {
                 pumpDisplayState.value = PumpDisplayState(name: pumpManager.localizedTitle, image: pumpManager.smallImage)
                 pumpName.send(pumpManager.localizedTitle)
-
+                pumpManager.setMustProvideBLEHeartbeat(heartbeatBypump)
                 if let omnipod = pumpManager as? OmnipodPumpManager {
                     guard let endTime = omnipod.state.podState?.expiresAt else {
                         pumpExpiresAtDate.send(nil)
                         return
                     }
-                    pumpManager.setMustProvideBLEHeartbeat(heartbeatBypump)
                     pumpExpiresAtDate.send(endTime)
                 }
                 if let omnipodBLE = pumpManager as? OmniBLEPumpManager {
@@ -99,7 +98,6 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
                         pumpExpiresAtDate.send(nil)
                         return
                     }
-                    pumpManager.setMustProvideBLEHeartbeat(heartbeatBypump)
                     pumpExpiresAtDate.send(endTime)
                 }
             } else {

File diff ditekan karena terlalu besar
+ 33 - 1
FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings


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

@@ -951,6 +951,12 @@ Enact a temp Basal or a temp target */
 /* Online or internal server */
 "Online or internal server" = "Online eller intern server";
 
+/* Add Fat */
+"Fat" = "Fett";
+
+/* Add Protein */
+"Protein" = "Protein";
+
 /* -------------- Developer settings ---------------------- */
 /* Debug options */
 

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

@@ -1,11 +1,13 @@
 import Foundation
 
 struct CarbsEntry: JSON, Equatable, Hashable {
+    let id: String?
     let createdAt: Date
     let carbs: Decimal
     let enteredBy: String?
 
     static let manual = "freeaps-x"
+    static let appleHealth = "applehealth"
 
     static func == (lhs: CarbsEntry, rhs: CarbsEntry) -> Bool {
         lhs.createdAt == rhs.createdAt
@@ -18,6 +20,7 @@ struct CarbsEntry: JSON, Equatable, Hashable {
 
 extension CarbsEntry {
     private enum CodingKeys: String, CodingKey {
+        case id = "_id"
         case createdAt = "created_at"
         case carbs
         case enteredBy

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

@@ -25,6 +25,11 @@ struct FreeAPSSettings: JSON, Equatable {
     var carbsRequiredThreshold: Decimal = 10
     var animatedBackground: Bool = false
     var displayStatistics: Bool = false
+    var useFPUconversion: Bool = false
+    var individualAdjustmentFactor: Decimal = 0.5
+    var timeCap: Decimal = 8
+    var minuteInterval: Int = 60
+    var delay: Int = 60
 }
 
 extension FreeAPSSettings: Decodable {
@@ -97,6 +102,26 @@ extension FreeAPSSettings: Decodable {
             settings.glucoseBadge = glucoseBadge
         }
 
+        if let useFPUconversion = try? container.decode(Bool.self, forKey: .useFPUconversion) {
+            settings.useFPUconversion = useFPUconversion
+        }
+
+        if let individualAdjustmentFactor = try? container.decode(Decimal.self, forKey: .individualAdjustmentFactor) {
+            settings.individualAdjustmentFactor = individualAdjustmentFactor
+        }
+
+        if let timeCap = try? container.decode(Decimal.self, forKey: .timeCap) {
+            settings.timeCap = timeCap
+        }
+
+        if let minuteInterval = try? container.decode(Int.self, forKey: .minuteInterval) {
+            settings.minuteInterval = minuteInterval
+        }
+
+        if let delay = try? container.decode(Int.self, forKey: .delay) {
+            settings.delay = delay
+        }
+
         if let glucoseNotificationsAlways = try? container.decode(Bool.self, forKey: .glucoseNotificationsAlways) {
             settings.glucoseNotificationsAlways = glucoseNotificationsAlways
         }

+ 42 - 4
FreeAPS/Sources/Modules/AddCarbs/AddCarbsStateModel.swift

@@ -4,23 +4,61 @@ extension AddCarbs {
     final class StateModel: BaseStateModel<Provider> {
         @Injected() var carbsStorage: CarbsStorage!
         @Injected() var apsManager: APSManager!
+        @Injected() var settings: SettingsManager!
         @Published var carbs: Decimal = 0
         @Published var date = Date()
+        @Published var protein: Decimal = 0
+        @Published var fat: Decimal = 0
         @Published var carbsRequired: Decimal?
+        @Published var useFPU: Bool = false
 
         override func subscribe() {
             carbsRequired = provider.suggestion?.carbsReq
+            useFPU = settingsManager.settings.useFPUconversion
         }
 
         func add() {
-            guard carbs > 0 else {
+            guard carbs > 0 || fat > 0 || protein > 0 else {
                 showModal(for: nil)
                 return
             }
 
-            carbsStorage.storeCarbs([
-                CarbsEntry(createdAt: date, carbs: carbs, enteredBy: CarbsEntry.manual)
-            ])
+            let interval = settings.settings.minuteInterval
+            let timeCap = settings.settings.timeCap * (60 / Decimal(interval))
+            let adjustment = settings.settings.individualAdjustmentFactor
+            let delay = settings.settings.delay
+
+            // Convert fat and protein to carb equivalents and store as future carbs
+            let fpucarb = 0.4 * protein + 0.9 * fat
+            let fpus = (fat * 9.0 + protein * 4.0) / 100.0
+            var counter: Decimal = (fpus * 2) - 1.0
+            counter = max(timeCap, counter)
+            var roundedCounter: Decimal = 0
+            NSDecimalRound(&roundedCounter, &counter, 0, .up)
+            let carbequiv = (fpucarb / roundedCounter) * adjustment
+            let firstDate = date.addingTimeInterval(delay.minutes.timeInterval)
+            var previousDate = date
+
+            while counter > 0, carbequiv > 0 {
+                var useDate = date + 1 * Double(interval * 60)
+                // Fix Interval and Delay
+                useDate = max(previousDate.addingTimeInterval(interval.minutes.timeInterval), useDate, firstDate)
+                if useDate > previousDate {
+                    carbsStorage.storeCarbs([
+                        CarbsEntry(
+                            id: UUID().uuidString, createdAt: useDate, carbs: carbequiv,
+                            enteredBy: CarbsEntry.manual
+                        )
+                    ])
+                }
+                previousDate = useDate
+                counter -= 1
+            }
+            // Store the real carbs
+            if carbs > 0 {
+                carbsStorage
+                    .storeCarbs([CarbsEntry(id: UUID().uuidString, createdAt: date, carbs: carbs, enteredBy: CarbsEntry.manual)])
+            }
 
             if settingsManager.settings.skipBolusScreenAfterCarbs {
                 apsManager.determineBasalSync()

+ 38 - 8
FreeAPS/Sources/Modules/AddCarbs/View/AddCarbsRootView.swift

@@ -25,19 +25,49 @@ extension AddCarbs {
                     }
                 }
                 Section {
-                    HStack {
-                        Text("Amount")
-                        Spacer()
-                        DecimalTextField("0", value: $state.carbs, formatter: formatter, autofocus: true, cleanInput: true)
-                        Text("grams").foregroundColor(.secondary)
+                    Section {
+                        HStack {
+                            Text("Carbs").fontWeight(.semibold)
+                            Spacer()
+                            DecimalTextField("0", value: $state.carbs, formatter: formatter, autofocus: true, cleanInput: true)
+                            Text("grams").foregroundColor(.secondary)
+                        }.padding(.vertical)
+
+                        // MARK: Adding Protein and Fat. Test
+
+                        if state.useFPU {
+                            HStack {
+                                Text("Protein").foregroundColor(.loopRed).fontWeight(.thin)
+                                Spacer()
+                                DecimalTextField(
+                                    "0",
+                                    value: $state.protein,
+                                    formatter: formatter,
+                                    autofocus: false,
+                                    cleanInput: true
+                                ).foregroundColor(.loopRed)
+                                Text("grams").foregroundColor(.secondary)
+                            }
+                            HStack {
+                                Text("Fat").foregroundColor(.loopYellow).fontWeight(.thin)
+                                Spacer()
+                                DecimalTextField(
+                                    "0",
+                                    value: $state.fat,
+                                    formatter: formatter,
+                                    autofocus: false,
+                                    cleanInput: true
+                                )
+                                Text("grams").foregroundColor(.secondary)
+                            }
+                        }
+                        DatePicker("Date", selection: $state.date)
                     }
-                    DatePicker("Date", selection: $state.date)
                 }
-
                 Section {
                     Button { state.add() }
                     label: { Text("Add") }
-                        .disabled(state.carbs <= 0)
+                        .disabled(state.carbs <= 0 && state.fat <= 0 && state.protein <= 0)
                 }
             }
             .onAppear(perform: configureView)

+ 9 - 4
FreeAPS/Sources/Modules/DataTable/DataTableDataFlow.swift

@@ -52,7 +52,8 @@ enum DataTable {
     }
 
     class Treatment: Identifiable, Hashable, Equatable {
-        let id = UUID()
+        let id: String
+        let idPumpEvent: String?
         let units: GlucoseUnits
         let type: DataType
         let date: Date
@@ -73,7 +74,9 @@ enum DataTable {
             date: Date,
             amount: Decimal? = nil,
             secondAmount: Decimal? = nil,
-            duration: Decimal? = nil
+            duration: Decimal? = nil,
+            id: String? = nil,
+            idPumpEvent: String? = nil
         ) {
             self.units = units
             self.type = type
@@ -81,6 +84,8 @@ enum DataTable {
             self.amount = amount
             self.secondAmount = secondAmount
             self.duration = duration
+            self.id = id ?? UUID().uuidString
+            self.idPumpEvent = idPumpEvent
         }
 
         static func == (lhs: Treatment, rhs: Treatment) -> Bool {
@@ -172,7 +177,7 @@ protocol DataTableProvider: Provider {
     func tempTargets() -> [TempTarget]
     func carbs() -> [CarbsEntry]
     func glucose() -> [BloodGlucose]
-    func deleteCarbs(at date: Date)
-    func deleteInsulin(at date: Date)
+    func deleteCarbs(_ treatement: DataTable.Treatment)
+    func deleteInsulin(_ treatement: DataTable.Treatment)
     func deleteGlucose(id: String)
 }

+ 9 - 5
FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift

@@ -21,12 +21,16 @@ extension DataTable {
             carbsStorage.recent()
         }
 
-        func deleteCarbs(at date: Date) {
-            nightscoutManager.deleteCarbs(at: date)
+        func deleteCarbs(_ treatement: Treatment) {
+            nightscoutManager.deleteCarbs(at: treatement.date)
+            healthkitManager.deleteCarbs(syncID: treatement.id)
         }
 
-        func deleteInsulin(at date: Date) {
-            nightscoutManager.deleteInsulin(at: date)
+        func deleteInsulin(_ treatement: Treatment) {
+            nightscoutManager.deleteInsulin(at: treatement.date)
+            if let id = treatement.idPumpEvent {
+                healthkitManager.deleteInsulin(syncID: id)
+            }
         }
 
         func glucose() -> [BloodGlucose] {
@@ -35,7 +39,7 @@ extension DataTable {
 
         func deleteGlucose(id: String) {
             glucoseStorage.removeGlucose(ids: [id])
-            healthkitManager.deleteGlucise(syncID: id)
+            healthkitManager.deleteGlucose(syncID: id)
         }
     }
 }

+ 16 - 6
FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -26,13 +26,23 @@ extension DataTable {
                 let units = self.settingsManager.settings.units
 
                 let carbs = self.provider.carbs().map {
-                    Treatment(units: units, type: .carbs, date: $0.createdAt, amount: $0.carbs)
+                    if let id = $0.id {
+                        return Treatment(
+                            units: units,
+                            type: .carbs,
+                            date: $0.createdAt,
+                            amount: $0.carbs,
+                            id: id
+                        )
+                    } else {
+                        return Treatment(units: units, type: .carbs, date: $0.createdAt, amount: $0.carbs)
+                    }
                 }
 
                 let boluses = self.provider.pumpHistory()
                     .filter { $0.type == .bolus }
                     .map {
-                        Treatment(units: units, type: .bolus, date: $0.timestamp, amount: $0.amount)
+                        Treatment(units: units, type: .bolus, date: $0.timestamp, amount: $0.amount, idPumpEvent: $0.id)
                     }
 
                 let tempBasals = self.provider.pumpHistory()
@@ -90,15 +100,15 @@ extension DataTable {
             }
         }
 
-        func deleteCarbs(at date: Date) {
-            provider.deleteCarbs(at: date)
+        func deleteCarbs(_ treatment: Treatment) {
+            provider.deleteCarbs(treatment)
         }
 
-        func deleteInsulin(at date: Date) {
+        func deleteInsulin(_ treatment: Treatment) {
             unlockmanager.unlock()
                 .sink { _ in } receiveValue: { [weak self] _ in
                     guard let self = self else { return }
-                    self.provider.deleteInsulin(at: date)
+                    self.provider.deleteInsulin(treatment)
                 }
                 .store(in: &lifetime)
         }

+ 2 - 2
FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift

@@ -94,7 +94,7 @@ extension DataTable {
                                 message: Text(item.amountText),
                                 primaryButton: .destructive(
                                     Text("Delete"),
-                                    action: { state.deleteCarbs(at: item.date) }
+                                    action: { state.deleteCarbs(item) }
                                 ),
                                 secondaryButton: .cancel()
                             )
@@ -116,7 +116,7 @@ extension DataTable {
                                 message: Text(item.amountText),
                                 primaryButton: .destructive(
                                     Text("Delete"),
-                                    action: { state.deleteInsulin(at: item.date) }
+                                    action: { state.deleteInsulin(item) }
                                 ),
                                 secondaryButton: .cancel()
                             )

+ 5 - 0
FreeAPS/Sources/Modules/FPUConfig/FPUConfigDataFlow.swift

@@ -0,0 +1,5 @@
+enum FPUConfig {
+    enum Config {}
+}
+
+protocol FPUConfigProvider {}

+ 3 - 0
FreeAPS/Sources/Modules/FPUConfig/FPUConfigProvider.swift

@@ -0,0 +1,3 @@
+extension FPUConfig {
+    final class Provider: BaseProvider, FPUConfigProvider {}
+}

+ 44 - 0
FreeAPS/Sources/Modules/FPUConfig/FPUConfigStateModel.swift

@@ -0,0 +1,44 @@
+import SwiftUI
+
+extension FPUConfig {
+    final class StateModel: BaseStateModel<Provider> {
+        @Published var useFPUconversion = false
+        @Published var individualAdjustmentFactor: Decimal = 0
+        @Published var timeCap: Decimal = 0
+        @Published var minuteInterval: Decimal = 0
+        @Published var delay: Decimal = 0
+
+        override func subscribe() {
+            subscribeSetting(\.useFPUconversion, on: $useFPUconversion) { useFPUconversion = $0 }
+            subscribeSetting(\.timeCap, on: $timeCap) { timeCap = $0 }
+
+            subscribeSetting(\.timeCap, on: $timeCap, initial: {
+                let value = max(min($0, 12), 8)
+                timeCap = value
+            }, map: {
+                $0
+            })
+
+            subscribeSetting(\.minuteInterval, on: $minuteInterval.map(Int.init), initial: {
+                let value = max(min($0, 60), 10)
+                minuteInterval = Decimal(value)
+            }, map: {
+                $0
+            })
+
+            subscribeSetting(\.delay, on: $delay.map(Int.init), initial: {
+                let value = max(min($0, 120), 60)
+                delay = Decimal(value)
+            }, map: {
+                $0
+            })
+
+            subscribeSetting(\.individualAdjustmentFactor, on: $individualAdjustmentFactor, initial: {
+                let value = max(min($0, 1.2), 0.1)
+                individualAdjustmentFactor = value
+            }, map: {
+                $0
+            })
+        }
+    }
+}

File diff ditekan karena terlalu besar
+ 64 - 0
FreeAPS/Sources/Modules/FPUConfig/View/FPUConfigRootView.swift


+ 1 - 1
FreeAPS/Sources/Modules/HealthKit/HealthKitStateModel.swift

@@ -35,7 +35,7 @@ extension AppleHealthKit {
 
                     debug(.service, "Permission  granted HealthKitManager")
 
-                    self.healthKitManager.createObserver()
+                    self.healthKitManager.createBGObserver()
                     self.healthKitManager.enableBackgroundDelivery()
                 }
             }

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

@@ -93,7 +93,7 @@ struct MainChartView: View {
     private var carbsFormatter: NumberFormatter {
         let formatter = NumberFormatter()
         formatter.numberStyle = .decimal
-        formatter.maximumFractionDigits = 0
+        formatter.maximumFractionDigits = 1
         return formatter
     }
 

+ 0 - 3
FreeAPS/Sources/Modules/Settings/SettingsStateModel.swift

@@ -11,11 +11,8 @@ extension Settings {
         @Published var animatedBackground = false
 
         private(set) var buildNumber = ""
-
         private(set) var versionNumber = ""
-
         private(set) var branch = ""
-
         private(set) var copyrightNotice = ""
 
         override func subscribe() {

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

@@ -29,6 +29,7 @@ extension Settings {
                         Text("Apple Health").navigationLink(to: .healthkit, from: self)
                     }
                     Text("Notifications").navigationLink(to: .notificationsConfig, from: self)
+                    Text("Fat And Protein Conversion").navigationLink(to: .fpuConfig, from: self)
                 }
 
                 Section(header: Text("Configuration")) {

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

@@ -25,6 +25,7 @@ enum Screen: Identifiable, Hashable {
     case libreConfig
     case calibrations
     case notificationsConfig
+    case fpuConfig
     case snooze
 
     var id: Int { String(reflecting: self).hashValue }
@@ -82,6 +83,8 @@ extension Screen {
             Calibrations.RootView(resolver: resolver)
         case .notificationsConfig:
             NotificationsConfig.RootView(resolver: resolver)
+        case .fpuConfig:
+            FPUConfig.RootView(resolver: resolver)
         case .snooze:
             Snooze.RootView(resolver: resolver)
         }

+ 238 - 15
FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift

@@ -1,6 +1,7 @@
 import Combine
 import Foundation
 import HealthKit
+import LoopKit
 import LoopKitUI
 import Swinject
 
@@ -14,21 +15,35 @@ protocol HealthKitManager: GlucoseSource {
     func requestPermission(completion: ((Bool, Error?) -> Void)?)
     /// Save blood glucose to Health store (dublicate of bg will ignore)
     func saveIfNeeded(bloodGlucose: [BloodGlucose])
+    /// Save carbs to Health store (dublicate of bg will ignore)
+    func saveIfNeeded(carbs: [CarbsEntry])
+    /// Save Insulin to Health store
+    func saveIfNeeded(pumpEvents events: [PumpHistoryEvent])
     /// Create observer for data passing beetwen Health Store and FreeAPS
-    func createObserver()
+    func createBGObserver()
     /// Enable background delivering objects from Apple Health to FreeAPS
     func enableBackgroundDelivery()
     /// Delete glucose with syncID
-    func deleteGlucise(syncID: String)
+    func deleteGlucose(syncID: String)
+    /// delete carbs with syncID
+    func deleteCarbs(syncID: String)
+    /// delete insulin with syncID
+    func deleteInsulin(syncID: String)
 }
 
-final class BaseHealthKitManager: HealthKitManager, Injectable {
+final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsObserver {
     private enum Config {
         // unwraped HKObjects
-        static var permissions: Set<HKSampleType> { Set([healthBGObject].compactMap { $0 }) }
+        static var readPermissions: Set<HKSampleType> {
+            Set([healthBGObject].compactMap { $0 }) }
+
+        static var writePermissions: Set<HKSampleType> {
+            Set([healthBGObject, healthCarbObject, healthInsulinObject].compactMap { $0 }) }
 
         // link to object in HealthKit
         static let healthBGObject = HKObjectType.quantityType(forIdentifier: .bloodGlucose)
+        static let healthCarbObject = HKObjectType.quantityType(forIdentifier: .dietaryCarbohydrates)
+        static let healthInsulinObject = HKObjectType.quantityType(forIdentifier: .insulinDelivery)
 
         // Meta-data key of FreeASPX data in HealthStore
         static let freeAPSMetaKey = "fromFreeAPSX"
@@ -37,6 +52,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
     @Injected() private var glucoseStorage: GlucoseStorage!
     @Injected() private var healthKitStore: HKHealthStore!
     @Injected() private var settingsManager: SettingsManager!
+    @Injected() private var broadcaster: Broadcaster!
 
     private let processQueue = DispatchQueue(label: "BaseHealthKitManager.processQueue")
     private var lifetime = Lifetime()
@@ -47,22 +63,25 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
     // last anchor for HKAnchoredQuery
     private var lastBloodGlucoseQueryAnchor: HKQueryAnchor? {
         set {
-            persistedAnchor = try? NSKeyedArchiver.archivedData(withRootObject: newValue as Any, requiringSecureCoding: false)
+            persistedBGAnchor = try? NSKeyedArchiver.archivedData(withRootObject: newValue as Any, requiringSecureCoding: false)
         }
         get {
-            guard let data = persistedAnchor else { return nil }
+            guard let data = persistedBGAnchor else { return nil }
             return try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? HKQueryAnchor
         }
     }
 
-    @Persisted(key: "HealthKitManagerAnchor") private var persistedAnchor: Data? = nil
+    @Persisted(key: "HealthKitManagerAnchor") private var persistedBGAnchor: Data? = nil
 
     var isAvailableOnCurrentDevice: Bool {
         HKHealthStore.isHealthDataAvailable()
     }
 
     var areAllowAllPermissions: Bool {
-        Set(Config.permissions.map { healthKitStore.authorizationStatus(for: $0) })
+        Set(Config.readPermissions.map { healthKitStore.authorizationStatus(for: $0) })
+            .intersection([.notDetermined])
+            .isEmpty &&
+            Set(Config.writePermissions.map { healthKitStore.authorizationStatus(for: $0) })
             .intersection([.sharingDenied, .notDetermined])
             .isEmpty
     }
@@ -91,8 +110,11 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
         injectServices(resolver)
         guard isAvailableOnCurrentDevice,
               Config.healthBGObject != nil else { return }
-        createObserver()
+        createBGObserver()
         enableBackgroundDelivery()
+
+        broadcaster.register(CarbsObserver.self, observer: self)
+
         debug(.service, "HealthKitManager did create")
     }
 
@@ -109,12 +131,12 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
             completion?(false, HKError.notAvailableOnCurrentDevice)
             return
         }
-        guard Config.permissions.isNotEmpty else {
+        guard Config.readPermissions.isNotEmpty, Config.writePermissions.isNotEmpty else {
             completion?(false, HKError.dataNotAvailable)
             return
         }
 
-        healthKitStore.requestAuthorization(toShare: Config.permissions, read: Config.permissions) { status, error in
+        healthKitStore.requestAuthorization(toShare: Config.writePermissions, read: Config.readPermissions) { status, error in
             completion?(status, error)
         }
     }
@@ -154,7 +176,130 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
             .store(in: &lifetime)
     }
 
-    func createObserver() {
+    func saveIfNeeded(carbs: [CarbsEntry]) {
+        guard settingsManager.settings.useAppleHealth,
+              let sampleType = Config.healthCarbObject,
+              checkAvailabilitySave(objectTypeToHealthStore: sampleType),
+              carbs.isNotEmpty
+        else { return }
+
+        let carbsWithId = carbs.filter { c in
+            guard c.id != nil else { return false }
+            return true
+        }
+
+        func save(samples: [HKSample]) {
+            let sampleIDs = samples.compactMap(\.syncIdentifier)
+            let sampleDates = samples.map(\.startDate)
+            let samplesToSave = carbsWithId
+                .filter { !sampleIDs.contains($0.id!) } // id existing in AH
+                .filter { !sampleDates.contains($0.createdAt) } // not id but exaclty the same datetime
+                .map {
+                    HKQuantitySample(
+                        type: sampleType,
+                        quantity: HKQuantity(unit: .gram(), doubleValue: Double($0.carbs)),
+                        start: $0.createdAt,
+                        end: $0.createdAt,
+                        metadata: [
+                            HKMetadataKeyExternalUUID: $0.id ?? "_id",
+                            HKMetadataKeySyncIdentifier: $0.id ?? "_id",
+                            HKMetadataKeySyncVersion: 1,
+                            Config.freeAPSMetaKey: true
+                        ]
+                    )
+                }
+
+            healthKitStore.save(samplesToSave) { _, _ in }
+        }
+
+        loadSamplesFromHealth(sampleType: sampleType)
+            .receive(on: processQueue)
+            .sink(receiveValue: save)
+            .store(in: &lifetime)
+    }
+
+    func saveIfNeeded(pumpEvents events: [PumpHistoryEvent]) {
+        guard settingsManager.settings.useAppleHealth,
+              let sampleType = Config.healthInsulinObject,
+              checkAvailabilitySave(objectTypeToHealthStore: sampleType),
+              events.isNotEmpty
+        else { return }
+
+        func save(bolus: [InsulinBolus], basal: [InsulinBasal]) {
+            let bolusSamples = bolus
+                .map {
+                    HKQuantitySample(
+                        type: sampleType,
+                        quantity: HKQuantity(unit: .internationalUnit(), doubleValue: Double($0.amount)),
+                        start: $0.date,
+                        end: $0.date,
+                        metadata: [
+                            HKMetadataKeyInsulinDeliveryReason: NSNumber(2),
+                            HKMetadataKeyExternalUUID: $0.id,
+                            HKMetadataKeySyncIdentifier: $0.id,
+                            HKMetadataKeySyncVersion: 1,
+                            Config.freeAPSMetaKey: true
+                        ]
+                    )
+                }
+
+            let basalSamples = basal
+                .map {
+                    HKQuantitySample(
+                        type: sampleType,
+                        quantity: HKQuantity(unit: .internationalUnit(), doubleValue: Double($0.amount)),
+                        start: $0.startDelivery,
+                        end: $0.endDelivery,
+                        metadata: [
+                            HKMetadataKeyInsulinDeliveryReason: NSNumber(1),
+                            HKMetadataKeyExternalUUID: $0.id,
+                            HKMetadataKeySyncIdentifier: $0.id,
+                            HKMetadataKeySyncVersion: 1,
+                            Config.freeAPSMetaKey: true
+                        ]
+                    )
+                }
+
+            healthKitStore.save(bolusSamples + basalSamples) { _, _ in }
+        }
+
+        loadSamplesFromHealth(sampleType: sampleType, withIDs: events.map(\.id))
+            .receive(on: processQueue)
+            .compactMap { samples -> ([InsulinBolus], [InsulinBasal]) in
+                let sampleIDs = samples.compactMap(\.syncIdentifier)
+                let bolus = events
+                    .filter { $0.type == .bolus && !sampleIDs.contains($0.id) }
+                    .compactMap { event -> InsulinBolus? in
+                        guard let amount = event.amount else { return nil }
+                        return InsulinBolus(id: event.id, amount: amount, date: event.timestamp)
+                    }
+                let basalEvents = events
+                    .filter { $0.type == .tempBasal && !sampleIDs.contains($0.id) }
+                let basal = basalEvents.enumerated()
+                    .compactMap { item -> InsulinBasal? in
+                        let nextElementEventIndex = item.offset + 1
+                        guard basalEvents.count > nextElementEventIndex else { return nil }
+                        let nextBasalEvent = basalEvents[nextElementEventIndex]
+                        let secondsOfCurrentBasal = nextBasalEvent.timestamp.timeIntervalSince(item.element.timestamp)
+                        let amount = Decimal(secondsOfCurrentBasal / 3600) * (item.element.rate ?? 0)
+                        let id = String(item.element.id.dropFirst())
+                        guard amount > 0,
+                              id != ""
+                        else { return nil }
+                        return InsulinBasal(
+                            id: id,
+                            amount: amount,
+                            startDelivery: item.element.timestamp,
+                            endDelivery: nextBasalEvent.timestamp
+                        )
+                    }
+                return (bolus, basal)
+            }
+            .sink(receiveValue: save)
+            .store(in: &lifetime)
+    }
+
+    func createBGObserver() {
         guard settingsManager.settings.useAppleHealth else { return }
 
         guard let bgType = Config.healthBGObject else {
@@ -201,6 +346,23 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
         }
     }
 
+    /// Try to load samples from Health store
+    private func loadSamplesFromHealth(
+        sampleType: HKQuantityType
+    ) -> Future<[HKSample], Never> {
+        Future { promise in
+            let query = HKSampleQuery(
+                sampleType: sampleType,
+                predicate: nil,
+                limit: 1000,
+                sortDescriptors: nil
+            ) { _, results, _ in
+                promise(.success((results as? [HKQuantitySample]) ?? []))
+            }
+            self.healthKitStore.execute(query)
+        }
+    }
+
     /// Try to load samples from Health store with id and do some work
     private func loadSamplesFromHealth(
         sampleType: HKQuantityType,
@@ -243,14 +405,14 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
                 if let bgSamples = addedObjects as? [HKQuantitySample],
                    bgSamples.isNotEmpty
                 {
-                    self.prepareSamplesToPublisherFetch(bgSamples)
+                    self.prepareBGSamplesToPublisherFetch(bgSamples)
                 }
             }
         }
         return query
     }
 
-    private func prepareSamplesToPublisherFetch(_ samples: [HKQuantitySample]) {
+    private func prepareBGSamplesToPublisherFetch(_ samples: [HKQuantitySample]) {
         dispatchPrecondition(condition: .onQueue(processQueue))
         debug(.service, "Start preparing samples: \(String(describing: samples))")
 
@@ -333,7 +495,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
         fetch(nil)
     }
 
-    func deleteGlucise(syncID: String) {
+    func deleteGlucose(syncID: String) {
         guard settingsManager.settings.useAppleHealth,
               let sampleType = Config.healthBGObject,
               checkAvailabilitySave(objectTypeToHealthStore: sampleType)
@@ -352,6 +514,54 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
             }
         }
     }
+
+    // - MARK Carbs function
+
+    func deleteCarbs(syncID: String) {
+        guard settingsManager.settings.useAppleHealth,
+              let sampleType = Config.healthCarbObject,
+              checkAvailabilitySave(objectTypeToHealthStore: sampleType)
+        else { return }
+
+        processQueue.async {
+            let predicate = HKQuery.predicateForObjects(
+                withMetadataKey: HKMetadataKeySyncIdentifier,
+                operatorType: .equalTo,
+                value: syncID
+            )
+
+            self.healthKitStore.deleteObjects(of: sampleType, predicate: predicate) { _, _, error in
+                guard let error = error else { return }
+                warning(.service, "Cannot delete sample with syncID: \(syncID)", error: error)
+            }
+        }
+    }
+
+    func carbsDidUpdate(_ carbs: [CarbsEntry]) {
+        saveIfNeeded(carbs: carbs)
+    }
+
+    // - MARK Insulin function
+
+    func deleteInsulin(syncID: String) {
+        guard settingsManager.settings.useAppleHealth,
+              let sampleType = Config.healthInsulinObject,
+              checkAvailabilitySave(objectTypeToHealthStore: sampleType)
+        else { return }
+
+        processQueue.async {
+            let predicate = HKQuery.predicateForObjects(
+                withMetadataKey: HKMetadataKeySyncIdentifier,
+                operatorType: .equalTo,
+                value: syncID
+            )
+
+            self.healthKitStore.deleteObjects(of: sampleType, predicate: predicate) { _, _, error in
+                guard let error = error else { return }
+                warning(.service, "Cannot delete sample with syncID: \(syncID)", error: error)
+            }
+        }
+    }
 }
 
 enum HealthKitPermissionRequestStatus {
@@ -365,3 +575,16 @@ enum HKError: Error {
     // Some data can be not available on current iOS-device
     case dataNotAvailable
 }
+
+private struct InsulinBolus {
+    var id: String
+    var amount: Decimal
+    var date: Date
+}
+
+private struct InsulinBasal {
+    var id: String
+    var amount: Decimal
+    var startDelivery: Date
+    var endDelivery: Date
+}

+ 1 - 1
FreeAPS/Sources/Services/WatchManager/WatchManager.swift

@@ -220,7 +220,7 @@ extension BaseWatchManager: WCSessionDelegate {
 
         if let carbs = message["carbs"] as? Double, carbs > 0 {
             carbsStorage.storeCarbs([
-                CarbsEntry(createdAt: Date(), carbs: Decimal(carbs), enteredBy: CarbsEntry.manual)
+                CarbsEntry(id: UUID().uuidString, createdAt: Date(), carbs: Decimal(carbs), enteredBy: CarbsEntry.manual)
             ])
 
             if settingsManager.settings.skipBolusScreenAfterCarbs {

+ 8 - 0
fastlane/testflight.md

@@ -70,6 +70,14 @@ _Please note that in default builds of FreeAPS X, the app group is actually iden
 1. Click "Confirm".
 1. Remember to do this for each of the identifiers above.
 
+## Add NFC Tag Reading to FreeeAPS App ID
+1. Go to [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/identifiers/list) on the apple developer site.
+1. Click on the "FreeeAPS" identifier
+1. Scroll down to "NFC Tag Reading"
+1. Tap the check box to enable NFC Tag Reading.
+1. Click "Save".
+1. Click "Confirm".
+
 ## Create FreeAPS X App in App Store Connect
 
 If you have created a FreeAPS X app in App Store Connect before, you can skip this section as well.