Преглед на файлове

Merge pull request #251 from Artificial-Pancreas/dev

Dev updates
Jon B Mårtensson преди 2 години
родител
ревизия
dd37f9cb42

+ 16 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -305,6 +305,8 @@
 		CA370FC152BC98B3D1832968 /* BasalProfileEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */; };
 		CC6C406E2ACDD69E009B8058 /* RawFetchedProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC6C406D2ACDD69E009B8058 /* RawFetchedProfile.swift */; };
 		CD78BB94E43B249D60CC1A1B /* NotificationsConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22963BD06A9C83959D4914E4 /* NotificationsConfigRootView.swift */; };
+		CE1856F52ADC4858007E39C7 /* AddCarbPresetIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1856F42ADC4858007E39C7 /* AddCarbPresetIntent.swift */; };
+		CE1856F72ADC4869007E39C7 /* CarbPresetIntentRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1856F62ADC4869007E39C7 /* CarbPresetIntentRequest.swift */; };
 		CE2FAD38297D69E1001A872C /* ShareClient.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CE398D1A297D69A900DF218F /* ShareClient.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		CE2FAD3A297D93F0001A872C /* BloodGlucoseExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2FAD39297D93F0001A872C /* BloodGlucoseExtensions.swift */; };
 		CE398D16297C9D1D00DF218F /* dexcomSourceG7.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE398D15297C9D1D00DF218F /* dexcomSourceG7.swift */; };
@@ -820,6 +822,8 @@
 		C377490C77661D75E8C50649 /* ManualTempBasalRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalRootView.swift; sourceTree = "<group>"; };
 		C8D1A7CA8C10C4403D4BBFA7 /* BolusDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BolusDataFlow.swift; sourceTree = "<group>"; };
 		CC6C406D2ACDD69E009B8058 /* RawFetchedProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RawFetchedProfile.swift; sourceTree = "<group>"; };
+		CE1856F42ADC4858007E39C7 /* AddCarbPresetIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddCarbPresetIntent.swift; sourceTree = "<group>"; };
+		CE1856F62ADC4869007E39C7 /* CarbPresetIntentRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbPresetIntentRequest.swift; sourceTree = "<group>"; };
 		CE2FAD39297D93F0001A872C /* BloodGlucoseExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloodGlucoseExtensions.swift; sourceTree = "<group>"; };
 		CE398D012977349800DF218F /* CryptoKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CryptoKit.framework; path = System/Library/Frameworks/CryptoKit.framework; sourceTree = SDKROOT; };
 		CE398D15297C9D1D00DF218F /* dexcomSourceG7.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dexcomSourceG7.swift; sourceTree = "<group>"; };
@@ -2058,9 +2062,19 @@
 			path = Bolus;
 			sourceTree = "<group>";
 		};
+		CE1856F32ADC4835007E39C7 /* Carbs */ = {
+			isa = PBXGroup;
+			children = (
+				CE1856F42ADC4858007E39C7 /* AddCarbPresetIntent.swift */,
+				CE1856F62ADC4869007E39C7 /* CarbPresetIntentRequest.swift */,
+			);
+			path = Carbs;
+			sourceTree = "<group>";
+		};
 		CE7CA3422A064973004BE681 /* Shortcuts */ = {
 			isa = PBXGroup;
 			children = (
+				CE1856F32ADC4835007E39C7 /* Carbs */,
 				CE7CA3432A064973004BE681 /* AppShortcuts.swift */,
 				CE7CA3442A064973004BE681 /* BaseIntentsRequest.swift */,
 				CE7CA3452A064973004BE681 /* TempPresets */,
@@ -2590,6 +2604,7 @@
 				388358C825EEF6D200E024B2 /* BasalProfileEntry.swift in Sources */,
 				3811DE0B25C9D32F00A708ED /* BaseView.swift in Sources */,
 				3811DE3225C9D49500A708ED /* HomeDataFlow.swift in Sources */,
+				CE1856F52ADC4858007E39C7 /* AddCarbPresetIntent.swift in Sources */,
 				38569347270B5DFB0002C50D /* CGMType.swift in Sources */,
 				3821ED4C25DD18BA00BC42AD /* Constants.swift in Sources */,
 				384E803425C385E60086DB71 /* JavaScriptWorker.swift in Sources */,
@@ -2664,6 +2679,7 @@
 				CE7CA3532A064973004BE681 /* tempPresetIntent.swift in Sources */,
 				D6DEC113821A7F1056C4AA1E /* NightscoutConfigDataFlow.swift in Sources */,
 				38E98A3025F52FF700C0CED0 /* Config.swift in Sources */,
+				CE1856F72ADC4869007E39C7 /* CarbPresetIntentRequest.swift in Sources */,
 				BD2B464E0745FBE7B79913F4 /* NightscoutConfigProvider.swift in Sources */,
 				9825E5E923F0B8FA80C8C7C7 /* NightscoutConfigStateModel.swift in Sources */,
 				38A43598262E0E4900E80935 /* FetchAnnouncementsManager.swift in Sources */,

+ 0 - 5
FreeAPS/Sources/APS/APSManager.swift

@@ -72,7 +72,6 @@ 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 {
@@ -268,10 +267,6 @@ 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 {

+ 33 - 5
FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift

@@ -158,21 +158,33 @@ extension NightscoutConfig {
                             return
                         }
 
+                        var areCRsOK = true
                         let carbratios = fetchedProfile.carbratio
                             .map { carbratio -> CarbRatioEntry in
-                                CarbRatioEntry(
+                                if carbratio.value <= 0 {
+                                    error =
+                                        "\nInvalid Carb Ratio settings in Nightscout.\n\nImport aborted. Please check your Nightscout Profile Carb Ratios Settings!"
+                                    areCRsOK = false
+                                }
+                                return CarbRatioEntry(
                                     start: carbratio.time,
                                     offset: (carbratio.timeAsSeconds ?? self.offset(carbratio.time)) / 60,
                                     ratio: carbratio.value
                                 ) }
                         let carbratiosProfile = CarbRatios(units: CarbUnit.grams, schedule: carbratios)
+                        guard areCRsOK else {
+                            group.leave()
+                            return
+                        }
 
                         var areBasalsOK = true
+                        let pumpName = self.apsManager.pumpName.value
                         let basals = fetchedProfile.basal
                             .map { basal -> BasalProfileEntry in
-                                if basal.value <= 0 || basal.value >= self.maxBasal {
+                                if pumpName != "Omnipod DASH", basal.value <= 0
+                                {
                                     error =
-                                        "\nInvalid Nightcsout Basal Settings. \n\nImport aborted. Please check your Nightscout Profile Basal Settings!"
+                                        "\nInvalid Nightcsout Basal Settings. Some or all of your basal settings are 0 U/h.\n\nImport aborted. Please check your Nightscout Profile Basal Settings before trying to import again. Import has been aborted.)"
                                     areBasalsOK = false
                                 }
                                 return BasalProfileEntry(
@@ -180,16 +192,32 @@ extension NightscoutConfig {
                                     minutes: (basal.timeAsSeconds ?? self.offset(basal.time)) / 60,
                                     rate: basal.value
                                 ) }
+                        // DASH pumps can have 0U/h basal rates but don't import if total basals (24 hours) amount to 0 U.
+                        if pumpName == "Omnipod DASH", basals.map({ each in each.rate }).reduce(0, +) <= 0
+                        {
+                            error =
+                                "\nYour total Basal insulin amount to 0 U or lower in Nightscout Profile settings.\n\n Please check your Nightscout Profile Basal Settings before trying to import again. Import has been aborted.)"
+                            areBasalsOK = false
+                        }
                         guard areBasalsOK else {
                             group.leave()
                             return
                         }
+
                         let sensitivities = fetchedProfile.sens.map { sensitivity -> InsulinSensitivityEntry in
                             InsulinSensitivityEntry(
                                 sensitivity: self.units == .mmolL ? sensitivity.value : sensitivity.value.asMgdL,
                                 offset: (sensitivity.timeAsSeconds ?? self.offset(sensitivity.time)) / 60,
                                 start: sensitivity.time
-                            ) }
+                            )
+                        }
+                        if sensitivities.filter({ $0.sensitivity <= 0 }).isNotEmpty {
+                            error =
+                                "\nInvalid Nightcsout Sensitivities Settings. \n\nImport aborted. Please check your Nightscout Profile Sensitivities Settings!"
+                            group.leave()
+                            return
+                        }
+
                         let sensitivitiesProfile = InsulinSensitivities(
                             units: self.units,
                             userPrefferedUnits: self.units,
@@ -238,7 +266,7 @@ extension NightscoutConfig {
                                 debug(.service, "Settings have been imported and the Basals saved to pump!")
                                 // DIA. Save if changed.
                                 let dia = fetchedProfile.dia
-                                if dia != self.dia {
+                                if dia != self.dia, dia <= 0 {
                                     let file = PumpSettings(
                                         insulinActionCurve: dia,
                                         maxBolus: self.maxBolus,

+ 44 - 3
FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift

@@ -31,7 +31,7 @@ protocol HealthKitManager: GlucoseSource {
     func deleteInsulin(syncID: String)
 }
 
-final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsObserver {
+final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsObserver, PumpHistoryObserver {
     private enum Config {
         // unwraped HKObjects
         static var readPermissions: Set<HKSampleType> {
@@ -68,7 +68,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsObserver {
         }
         get {
             guard let data = persistedBGAnchor else { return nil }
-            return try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? HKQueryAnchor
+            return try? NSKeyedUnarchiver.unarchivedObject(ofClass: HKQueryAnchor.self, from: data)
         }
     }
 
@@ -115,6 +115,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsObserver {
         enableBackgroundDelivery()
 
         broadcaster.register(CarbsObserver.self, observer: self)
+        broadcaster.register(PumpHistoryObserver.self, observer: self)
 
         debug(.service, "HealthKitManager did create")
     }
@@ -226,6 +227,21 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsObserver {
               events.isNotEmpty
         else { return }
 
+        func delete(syncIds: [String]?) {
+            syncIds?.forEach { syncID in
+                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 save(bolus: [InsulinBolus], basal: [InsulinBasal]) {
             let bolusSamples = bolus
                 .map {
@@ -264,6 +280,26 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsObserver {
             healthKitStore.save(bolusSamples + basalSamples) { _, _ in }
         }
 
+        // delete existing event in HK where the amount is not the last value in the pumphistory
+        loadSamplesFromHealth(sampleType: sampleType, withIDs: events.map(\.id))
+            .receive(on: processQueue)
+            .compactMap { samples -> [String] in
+                let sampleIDs = samples.compactMap(\.syncIdentifier)
+                let bolusToDelete = events
+                    .filter { $0.type == .bolus && sampleIDs.contains($0.id) }
+                    .compactMap { event -> String? in
+                        guard let amount = event.amount else { return nil }
+                        guard let sampleAmount = samples.first(where: { $0.syncIdentifier == event.id }) as? HKQuantitySample
+                        else { return nil }
+                        if Double(amount) != sampleAmount.quantity.doubleValue(for: .internationalUnit()) {
+                            return sampleAmount.syncIdentifier
+                        } else { return nil }
+                    }
+                return bolusToDelete
+            }
+            .sink(receiveValue: delete)
+            .store(in: &lifetime)
+
         loadSamplesFromHealth(sampleType: sampleType, withIDs: events.map(\.id))
             .receive(on: processQueue)
             .compactMap { samples -> ([InsulinBolus], [InsulinBasal]) in
@@ -276,6 +312,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsObserver {
                     }
                 let basalEvents = events
                     .filter { $0.type == .tempBasal && !sampleIDs.contains($0.id) }
+                    .sorted(by: { $0.timestamp < $1.timestamp })
                 let basal = basalEvents.enumerated()
                     .compactMap { item -> InsulinBasal? in
                         let nextElementEventIndex = item.offset + 1
@@ -300,7 +337,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsObserver {
                         }
 
                         let id = String(item.element.id.dropFirst())
-                        guard amountRounded >= 0,
+                        guard amountRounded > 0,
                               id != ""
                         else { return nil }
 
@@ -317,6 +354,10 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsObserver {
             .store(in: &lifetime)
     }
 
+    func pumpHistoryDidUpdate(_ events: [PumpHistoryEvent]) {
+        saveIfNeeded(pumpEvents: events)
+    }
+
     func createBGObserver() {
         guard settingsManager.settings.useAppleHealth else { return }
 

+ 1 - 1
FreeAPS/Sources/Services/Network/NightscoutManager.swift

@@ -603,7 +603,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                         self.storage.save(glucose, as: fileToSave)
                         debug(.nightscout, "Glucose uploaded")
                     case let .failure(error):
-                        debug(.nightscout, error.localizedDescription)
+                        debug(.nightscout, "Upload of glucose failed: " + error.localizedDescription)
                     }
                 } receiveValue: {}
                 .store(in: &self.lifetime)

+ 7 - 0
FreeAPS/Sources/Shortcuts/AppShortcuts.swift

@@ -17,5 +17,12 @@ import Foundation
                 "\(.applicationName) state"
             ]
         )
+        AppShortcut(
+            intent: AddCarbPresentIntent(),
+            phrases: [
+                "Add carbs in \(.applicationName)",
+                "\(.applicationName) allows to add carbs"
+            ]
+        )
     }
 }

+ 3 - 0
FreeAPS/Sources/Shortcuts/BaseIntentsRequest.swift

@@ -10,6 +10,9 @@ import Swinject
     @Injected() var settingsManager: SettingsManager!
     @Injected() var storage: TempTargetsStorage!
     @Injected() var fileStorage: FileStorage!
+    @Injected() var carbsStorage: CarbsStorage!
+    @Injected() var glucoseStorage: GlucoseStorage!
+    @Injected() var apsManager: APSManager!
 
     let resolver: Resolver
 

+ 94 - 0
FreeAPS/Sources/Shortcuts/Carbs/AddCarbPresetIntent.swift

@@ -0,0 +1,94 @@
+import AppIntents
+import Foundation
+import Intents
+import Swinject
+
+@available(iOS 16.0,*) struct AddCarbPresentIntent: AppIntent {
+    // Title of the action in the Shortcuts app
+    static var title: LocalizedStringResource = "Add carbs"
+
+    // Description of the action in the Shortcuts app
+    static var description = IntentDescription("Allow to add carbs in iAPS.")
+
+    internal var carbRequest: CarbPresetIntentRequest
+
+    init() {
+        carbRequest = CarbPresetIntentRequest()
+        dateAdded = Date()
+    }
+
+    @Parameter(
+        title: "Quantity Carbs",
+        description: "Quantity of carbs in g",
+        controlStyle: .field,
+        inclusiveRange: (lowerBound: 0, upperBound: 200),
+        requestValueDialog: IntentDialog("What is the numeric value of the carb to add")
+    ) var carbQuantity: Double?
+
+    @Parameter(
+        title: "Quantity fat",
+        description: "Quantity of fat in g",
+        default: 0.0,
+        inclusiveRange: (0, 200)
+    ) var fatQuantity: Double
+
+    @Parameter(
+        title: "Quantity Protein",
+        description: "Quantity of Protein in g",
+        default: 0.0,
+        inclusiveRange: (0, 200)
+    ) var proteinQuantity: Double
+
+    @Parameter(
+        title: "Date",
+        description: "Date of adding"
+    ) var dateAdded: Date
+
+    @Parameter(
+        title: "Confirm Before applying",
+        description: "If toggled, you will need to confirm before applying",
+        default: true
+    ) var confirmBeforeApplying: Bool
+
+    static var parameterSummary: some ParameterSummary {
+        When(\.$confirmBeforeApplying, .equalTo, true, {
+            Summary("Applying \(\.$carbQuantity) at \(\.$dateAdded)") {
+                \.$fatQuantity
+                \.$proteinQuantity
+                \.$confirmBeforeApplying
+            }
+        }, otherwise: {
+            Summary("Immediately applying \(\.$carbQuantity) at \(\.$dateAdded)") {
+                \.$fatQuantity
+                \.$proteinQuantity
+                \.$confirmBeforeApplying
+            }
+        })
+    }
+
+    @MainActor func perform() async throws -> some ProvidesDialog {
+        do {
+            let quantityCarbs: Double
+            if let cq = carbQuantity {
+                quantityCarbs = cq
+            } else {
+                quantityCarbs = try await $carbQuantity.requestValue("How many carbs ?")
+            }
+
+            let quantityCarbsName = quantityCarbs.toString()
+            if confirmBeforeApplying {
+                try await requestConfirmation(
+                    result: .result(dialog: "Are you sure to add \(quantityCarbsName) g of carbs ?")
+                )
+            }
+
+            let finalQuantityCarbsDisplay = try carbRequest.addCarbs(quantityCarbs, fatQuantity, proteinQuantity, dateAdded)
+            return .result(
+                dialog: IntentDialog(stringLiteral: finalQuantityCarbsDisplay)
+            )
+
+        } catch {
+            throw error
+        }
+    }
+}

+ 36 - 0
FreeAPS/Sources/Shortcuts/Carbs/CarbPresetIntentRequest.swift

@@ -0,0 +1,36 @@
+import CoreData
+import Foundation
+
+@available(iOS 16.0,*) final class CarbPresetIntentRequest: BaseIntentsRequest {
+    func addCarbs(_ quantityCarbs: Double, _ quantityFat: Double, _ quantityProtein: Double, _ dateAdded: Date) throws -> String {
+        guard quantityCarbs >= 0.0 || quantityFat >= 0.0 || quantityProtein >= 0.0 else {
+            return "no adding carbs in iAPS"
+        }
+
+        let carbs = min(Decimal(quantityCarbs), settingsManager.settings.maxCarbs)
+
+        carbsStorage.storeCarbs(
+            [CarbsEntry(
+                id: UUID().uuidString,
+                createdAt: dateAdded,
+                carbs: carbs,
+                fat: Decimal(quantityFat),
+                protein: Decimal(quantityProtein),
+                note: "add with shortcuts",
+                enteredBy: CarbsEntry.manual,
+                isFPU: false, fpuID: nil
+            )]
+        )
+        var resultDisplay: String
+        resultDisplay = "\(carbs) g carbs"
+        if quantityFat > 0.0 {
+            resultDisplay = "\(resultDisplay) and \(quantityFat) g fats"
+        }
+        if quantityProtein > 0.0 {
+            resultDisplay = "\(resultDisplay) and \(quantityProtein) g protein"
+        }
+        let dateName = dateAdded.formatted()
+        resultDisplay = "\(resultDisplay) added at \(dateName)"
+        return resultDisplay
+    }
+}

+ 0 - 4
FreeAPS/Sources/Shortcuts/State/StateIntentRequest.swift

@@ -54,10 +54,6 @@ enum StateIntentError: Error {
 }
 
 @available(iOS 16.0, *) final class StateIntentRequest: BaseIntentsRequest {
-    @Injected() private var glucoseStorage: GlucoseStorage!
-    @Injected() private var carbsStorage: CarbsStorage!
-    @Injected() private var apsManager: APSManager!
-
     func getLastBG() throws -> (dateGlucose: Date, glucose: String, trend: String, delta: String) {
         let glucose = glucoseStorage.recent()
         guard let lastGlucose = glucose.last, let glucoseValue = lastGlucose.glucose else { throw StateIntentError.NoBG }