From 28586cc3f8f25eb0f3d6d7cf35d1f1169b4bd713 Mon Sep 17 00:00:00 2001 From: Shawn Date: Fri, 31 Dec 2021 08:09:09 -0500 Subject: [PATCH 1/8] Add carbs to health kit --- .../Sources/APS/FetchTreatmentsManager.swift | 2 + .../Sources/APS/Storage/CarbsStorage.swift | 36 ++++ FreeAPS/Sources/Models/CarbsEntry.swift | 7 +- FreeAPS/Sources/Models/HealthKitSample.swift | 2 + .../Modules/DataTable/DataTableProvider.swift | 3 +- .../DataTable/View/DataTableRootView.swift | 4 +- .../Services/HealthKit/HealthKitManager.swift | 174 ++++++++++++++++-- 7 files changed, 208 insertions(+), 20 deletions(-) diff --git a/FreeAPS/Sources/APS/FetchTreatmentsManager.swift b/FreeAPS/Sources/APS/FetchTreatmentsManager.swift index 81be5a6b9..54d55eaed 100644 --- a/FreeAPS/Sources/APS/FetchTreatmentsManager.swift +++ b/FreeAPS/Sources/APS/FetchTreatmentsManager.swift @@ -10,6 +10,7 @@ final class BaseFetchTreatmentsManager: FetchTreatmentsManager, Injectable { @Injected() var nightscoutManager: NightscoutManager! @Injected() var tempTargetsStorage: TempTargetsStorage! @Injected() var carbsStorage: CarbsStorage! + @Injected() var healthKitManager: HealthKitManager! private var lifetime = Lifetime() private let timer = DispatchTimer(timeInterval: 1.minutes.timeInterval) @@ -34,6 +35,7 @@ final class BaseFetchTreatmentsManager: FetchTreatmentsManager, Injectable { let filteredCarbs = carbs.filter { !($0.enteredBy?.contains(CarbsEntry.manual) ?? false) } if filteredCarbs.isNotEmpty { self.carbsStorage.storeCarbs(filteredCarbs) + self.healthKitManager.saveIfNeeded(carbs: filteredCarbs) } let filteredTargets = targets.filter { !($0.enteredBy?.contains(TempTarget.manual) ?? false) } if filteredTargets.isNotEmpty { diff --git a/FreeAPS/Sources/APS/Storage/CarbsStorage.swift b/FreeAPS/Sources/APS/Storage/CarbsStorage.swift index 16c23f10d..e234ed554 100644 --- a/FreeAPS/Sources/APS/Storage/CarbsStorage.swift +++ b/FreeAPS/Sources/APS/Storage/CarbsStorage.swift @@ -11,6 +11,8 @@ protocol CarbsStorage { func syncDate() -> Date func recent() -> [CarbsEntry] func nightscoutTretmentsNotUploaded() -> [NigtscoutTreatment] + func removeCarbs(byDate date: Date) + func removeCarbs(byDateCollection dates: [Date]) func deleteCarbs(at date: Date) } @@ -39,6 +41,40 @@ final class BaseCarbsStorage: CarbsStorage, Injectable { } } } + + func removeCarbs(byDateCollection dates: [Date]) { + processQueue.sync { + let file = OpenAPS.Monitor.carbHistory + self.storage.transaction { storage in + let CarbInStorage = storage.retrieve(file, as: [CarbsEntry].self) + let filteredCarb = CarbInStorage?.filter { !ids.contains($0.id) } ?? [] + storage.save(filteredCarb, as: file) + + DispatchQueue.main.async { + self.broadcaster.notify(CarbsObserver.self, on: .main) { + $0.carbsDidUpdate(filteredCarb.reversed()) + } + } + } + } + } + + func removeCarbs(byDate date: Date) { + processQueue.sync { + let file = OpenAPS.Monitor.carbHistory + self.storage.transaction { storage in + let CarbInStorage = storage.retrieve(file, as: [CarbsEntry].self) + let filteredCarb = CarbInStorage?.filter { $0.id != id } ?? [] + storage.save(filteredCarb, as: file) + + DispatchQueue.main.async { + self.broadcaster.notify(CarbsObserver.self, on: .main) { + $0.carbsDidUpdate((filteredCarb.reversed()) + } + } + } + } + } func syncDate() -> Date { Date().addingTimeInterval(-1.days.timeInterval) diff --git a/FreeAPS/Sources/Models/CarbsEntry.swift b/FreeAPS/Sources/Models/CarbsEntry.swift index dbcbc237f..9c4033c4e 100644 --- a/FreeAPS/Sources/Models/CarbsEntry.swift +++ b/FreeAPS/Sources/Models/CarbsEntry.swift @@ -4,7 +4,12 @@ struct CarbsEntry: JSON, Equatable, Hashable { let createdAt: Date let carbs: Decimal let enteredBy: String? - + + var _id = UUID().uuidString + var id: String { + _id + } + static let manual = "freeaps-x" static func == (lhs: CarbsEntry, rhs: CarbsEntry) -> Bool { diff --git a/FreeAPS/Sources/Models/HealthKitSample.swift b/FreeAPS/Sources/Models/HealthKitSample.swift index 58b3f6845..b7c18ae4a 100644 --- a/FreeAPS/Sources/Models/HealthKitSample.swift +++ b/FreeAPS/Sources/Models/HealthKitSample.swift @@ -4,6 +4,7 @@ struct HealthKitSample: JSON, Hashable, Equatable { var healthKitId: String var date: Date var glucose: Int + var carb: Int static func == (lhs: HealthKitSample, rhs: HealthKitSample) -> Bool { lhs.healthKitId == rhs.healthKitId @@ -15,5 +16,6 @@ extension HealthKitSample { case healthKitId = "healthkit_id" case date case glucose + case carb } } diff --git a/FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift b/FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift index 134031c3b..baff67c1b 100644 --- a/FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift +++ b/FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift @@ -23,6 +23,7 @@ extension DataTable { func deleteCarbs(at date: Date) { nightscoutManager.deleteCarbs(at: date) + healthkitManager.deleteCarb(at: date) } func glucose() -> [BloodGlucose] { @@ -31,7 +32,7 @@ extension DataTable { func deleteGlucose(id: String) { glucoseStorage.removeGlucose(ids: [id]) - healthkitManager.deleteGlucise(syncID: id) + healthkitManager.deleteGlucose(syncID: id) } } } diff --git a/FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift b/FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift index 1c5bb6cfc..3bcea073e 100644 --- a/FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift +++ b/FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift @@ -63,7 +63,7 @@ extension DataTable { private var glucoseList: some View { List { ForEach(state.glucose) { item in - gluciseView(item) + glucoseView(item) }.onDelete(perform: deleteGlucose) } } @@ -103,7 +103,7 @@ extension DataTable { } } - @ViewBuilder private func gluciseView(_ item: Glucose) -> some View { + @ViewBuilder private func glucoseView(_ item: Glucose) -> some View { VStack(alignment: .leading, spacing: 4) { HStack { Text(dateFormatter.string(from: item.glucose.dateString)) diff --git a/FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift b/FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift index 869ea4df1..baf887600 100644 --- a/FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift +++ b/FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift @@ -3,7 +3,7 @@ import Foundation import HealthKit import Swinject -protocol HealthKitManager: GlucoseSource { +protocol HealthKitManager: GlucoseSource, CarbsEntry { /// Check all needed permissions /// Return false if one or more permissions are deny or not choosen var areAllowAllPermissions: Bool { get } @@ -13,27 +13,32 @@ protocol HealthKitManager: GlucoseSource { func requestPermission(completion: ((Bool, Error?) -> Void)?) /// Save blood glucose to Health store (dublicate of bg will ignore) func saveIfNeeded(bloodGlucose: [BloodGlucose]) + func saveIfNeeded(carbs: [CarbsEntry]) /// Create observer for data passing beetwen Health Store and FreeAPS func createObserver() /// Enable background delivering objects from Apple Health to FreeAPS func enableBackgroundDelivery() /// Delete glucose with syncID - func deleteGlucise(syncID: String) + func deleteGlucose(syncID: String) + func deleteCarb(at: Date) } final class BaseHealthKitManager: HealthKitManager, Injectable { private enum Config { // unwraped HKObjects - static var permissions: Set { Set([healthBGObject].compactMap { $0 }) } + static var permissions: Set { Set([healthBGObject].compactMap [healthCarbObject].compactMap { $0 }) } // link to object in HealthKit static let healthBGObject = HKObjectType.quantityType(forIdentifier: .bloodGlucose) + + static let healthCarbObject = HKObjectType.quantityType(forIdentifier: .dietaryCarbohydrates) // Meta-data key of FreeASPX data in HealthStore static let freeAPSMetaKey = "fromFreeAPSX" } @Injected() private var glucoseStorage: GlucoseStorage! + @Injected() private var carbsStorage: CarbsStorage! @Injected() private var healthKitStore: HKHealthStore! @Injected() private var settingsManager: SettingsManager! @@ -42,9 +47,11 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { // BG that will be return Publisher @SyncAccess @Persisted(key: "BaseHealthKitManager.newGlucose") private var newGlucose: [BloodGlucose] = [] + + @SyncAccess @Persisted(key: "BaseHealthKitManager.newCarb") private var newCarb: [CarbsEntry] = [] // last anchor for HKAnchoredQuery - private var lastBloodGlucoseQueryAnchor: HKQueryAnchor? { + private var lastQueryAnchor: HKQueryAnchor? { set { persistedAnchor = try? NSKeyedArchiver.archivedData(withRootObject: newValue as Any, requiringSecureCoding: false) } @@ -66,16 +73,16 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { .isEmpty } - // NSPredicate, which use during load increment BG from Health store - private var loadBGPredicate: NSPredicate { - // loading only daily bg + // NSPredicate, which use during load increment values from Health store + private var loadValuePredicate: NSPredicate { + // loading only daily values let predicateByStartDate = HKQuery.predicateForSamples( withStart: Date().addingTimeInterval(-1.days.timeInterval), end: nil, options: .strictStartDate ) - // loading only not FreeAPS bg + // loading only not FreeAPS values // this predicate dont influence on Deleted Objects, only on added let predicateByMeta = HKQuery.predicateForObjects( withMetadataKey: Config.freeAPSMetaKey, @@ -86,6 +93,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { return NSCompoundPredicate(andPredicateWithSubpredicates: [predicateByStartDate, predicateByMeta]) } + init(resolver: Resolver) { injectServices(resolver) guard isAvailableOnCurrentDevice, @@ -160,8 +168,27 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { warning(.service, "Can not create HealthKit Observer, because unable to get the Blood Glucose type") return } + + guard let carbType = Config.healthCarbObject else { + warning(.service, "Can not create HealthKit Observer, because unable to get the Carb type") + return + } + + let glucoseQuery = HKObserverQuery(sampleType: bgType, predicate: nil) { [weak self] _, _, observerError in + guard let self = self else { return } + debug(.service, "Execute HelathKit observer query for loading increment samples") + guard observerError == nil else { + warning(.service, "Error during execution of HelathKit Observer's query", error: observerError!) + return + } - let query = HKObserverQuery(sampleType: bgType, predicate: nil) { [weak self] _, _, observerError in + if let incrementQuery = self.getBloodGlucoseHKQuery(predicate: self.loadValuePredicate) { + debug(.service, "Create increment query") + self.healthKitStore.execute(incrementQuery) + } + } + + let carbQuery = HKObserverQuery(sampleType: carbType, predicate: nil) { [weak self] _, _, observerError in guard let self = self else { return } debug(.service, "Execute HelathKit observer query for loading increment samples") guard observerError == nil else { @@ -169,12 +196,14 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { return } - if let incrementQuery = self.getBloodGlucoseHKQuery(predicate: self.loadBGPredicate) { + if let incrementQuery = self.getCarbHKQuery(predicate: self.loadValuePredicate) { debug(.service, "Create increment query") self.healthKitStore.execute(incrementQuery) } } - healthKitStore.execute(query) + + healthKitStore.execute(glucoseQuery) + healthKitStore.execute(carbQuery) debug(.service, "Create Observer for Blood Glucose") } @@ -229,27 +258,53 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { let query = HKAnchoredObjectQuery( type: sampleType, predicate: predicate, - anchor: lastBloodGlucoseQueryAnchor, + anchor: lastQueryAnchor, limit: HKObjectQueryNoLimit ) { [weak self] _, addedObjects, _, anchor, _ in guard let self = self else { return } self.processQueue.async { debug(.service, "AnchoredQuery did execute") - self.lastBloodGlucoseQueryAnchor = anchor + self.lastQueryAnchor = anchor + + // Added objects + if let carbSamples = addedObjects as? [HKQuantitySample], + carbSamples.isNotEmpty + { + self.prepareBGSamplesToPublisherFetch(carbSamples) + } + } + } + return query + } + + private func getCarbHKQuery(predicate: NSPredicate) -> HKQuery? { + guard let sampleType = Config.healthCarbObject else { return nil } + + let query = HKAnchoredObjectQuery( + type: sampleType, + predicate: predicate, + anchor: lastQueryAnchor, + limit: HKObjectQueryNoLimit + ) { [weak self] _, addedObjects, _, anchor, _ in + guard let self = self else { return } + self.processQueue.async { + debug(.service, "AnchoredQuery did execute") + + self.lastQueryAnchor = anchor // Added objects if let bgSamples = addedObjects as? [HKQuantitySample], bgSamples.isNotEmpty { - self.prepareSamplesToPublisherFetch(bgSamples) + self.prepareCarbSamplesToPublisherFetch(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))") @@ -286,6 +341,38 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { "Current BloodGlucose.Type objects will be send from Publisher during fetch: \(String(describing: newGlucose))" ) } + + private func prepareCarbSamplesToPublisherFetch(_ samples: [HKQuantitySample]) { + dispatchPrecondition(condition: .onQueue(processQueue)) + debug(.service, "Start preparing samples: \(String(describing: samples))") + + newCarb += samples + .compactMap { sample -> HealthKitSample? in + //let fromFAX = sample.metadata?[Config.freeAPSMetaKey] as? Bool ?? false + //guard !fromFAX else { return nil } + return HealthKitSample( + healthKitId: sample.uuid.uuidString, + date: sample.startDate, + carb: sample.quantity.doubleValue(for: .gram()) + ) + } + .map { sample in + CarbsEntry( + _id: sample.healthKitId, + createdAt: Decimal(Int(sample.date.timeIntervalSince1970) * 1000), + carbs: sample.carb, + enteredBy: nil + ) + } + .filter { $0.dateString >= Date().addingTimeInterval(-1.days.timeInterval) } + + newCarb = newCarb.removeDublicates() + + debug( + .service, + "Current Carb.Type objects will be send from Publisher during fetch: \(String(describing: newCarb))" + ) + } func fetch() -> AnyPublisher<[BloodGlucose], Never> { Future { [weak self] promise in @@ -321,8 +408,43 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { } .eraseToAnyPublisher() } + + func fetch() -> AnyPublisher<[CarbsEntry], Never> { + Future { [weak self] promise in + guard let self = self else { + promise(.success([])) + return + } + + self.processQueue.async { + debug(.service, "Start fetching HealthKitManager") + guard self.settingsManager.settings.useAppleHealth else { + debug(.service, "HealthKitManager cant return any data, because useAppleHealth option is disable") + promise(.success([])) + return + } + + // Remove old BGs + self.newCarb = self.newCarb + .filter { $0.dateString >= Date().addingTimeInterval(-1.days.timeInterval) } + // Get actual BGs (beetwen Date() - 1 day and Date()) + let actualCarb = self.newCarb + .filter { $0.dateString <= Date() } + // Update newCarb + self.newCarb = self.newCarb + .filter { !actualCarb.contains($0) } + + debug(.service, "Actual carb is \(actualCarb)") + + debug(.service, "Current state of newCarb is \(self.newCarb)") + + promise(.success(actualCarb)) + } + } + .eraseToAnyPublisher() + } - func deleteGlucise(syncID: String) { + func deleteGlucose(syncID: String) { guard settingsManager.settings.useAppleHealth, let sampleType = Config.healthBGObject, checkAvailabilitySave(objectTypeToHealthStore: sampleType) @@ -341,6 +463,26 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { } } } + + func deleteCarb(at: Date) { + guard settingsManager.settings.useAppleHealth, + let sampleType = Config.healthCarbObject, + checkAvailabilitySave(objectTypeToHealthStore: sampleType) + else { return } + + processQueue.async { + let predicate = HKQuery.predicateForObjects( + withMetadataKey: HKMetadataKeySyncIdentifier, + operatorType: .equalTo, + value: at + ) + + 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 { From 0311c5ae1f14f9c48dd4006b50bb3e46430df84a Mon Sep 17 00:00:00 2001 From: Shawn Date: Fri, 31 Dec 2021 15:26:47 -0500 Subject: [PATCH 2/8] Refactor and properly fetch glucose --- FreeAPS.xcodeproj/project.pbxproj | 6 + .../Sources/APS/FetchTreatmentsManager.swift | 12 +- FreeAPS/Sources/APS/Storage/CarbSource.swift | 5 + .../Sources/APS/Storage/CarbsStorage.swift | 67 +++++----- FreeAPS/Sources/Models/CarbsEntry.swift | 10 +- FreeAPS/Sources/Models/HealthKitSample.swift | 4 +- .../Services/HealthKit/HealthKitManager.swift | 117 ++++++++++++------ 7 files changed, 139 insertions(+), 82 deletions(-) create mode 100644 FreeAPS/Sources/APS/Storage/CarbSource.swift diff --git a/FreeAPS.xcodeproj/project.pbxproj b/FreeAPS.xcodeproj/project.pbxproj index 166e418f4..793109546 100644 --- a/FreeAPS.xcodeproj/project.pbxproj +++ b/FreeAPS.xcodeproj/project.pbxproj @@ -20,6 +20,7 @@ 1D845DF2E3324130E1D95E67 /* DataTableProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60744C3E9BB3652895C908CC /* DataTableProvider.swift */; }; 23888883D4EA091C88480FF2 /* BolusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C19984D62EFC0035A9E9644D /* BolusProvider.swift */; }; 28089E07169488CF6DCC2A31 /* AddCarbsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86FC1CFD647CF34508AF9A3B /* AddCarbsRootView.swift */; }; + 29AC4F68277F8A2100766404 /* CarbSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29AC4F67277F8A2100766404 /* CarbSource.swift */; }; 2BE9A6FA20875F6F4F9CD461 /* PumpSettingsEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97F14812C1AFED3621165A5 /* PumpSettingsEditorProvider.swift */; }; 3083261C4B268E353F36CD0B /* AutotuneConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DCCCCE633F5E98E41B0CD3C /* AutotuneConfigDataFlow.swift */; }; 3171D2818C7C72CD1584BB5E /* NotificationsConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC2C6489D29ECCCAD78E0721 /* NotificationsConfigStateModel.swift */; }; @@ -439,6 +440,8 @@ 212E8BFE6D66EE65AA26A114 /* CalibrationsProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CalibrationsProvider.swift; sourceTree = ""; }; 223EC0494F55A91E3EA69EF4 /* BolusStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BolusStateModel.swift; sourceTree = ""; }; 22963BD06A9C83959D4914E4 /* NotificationsConfigRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NotificationsConfigRootView.swift; sourceTree = ""; }; + 29AC4F65277F48C100766404 /* ConfigOverride.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ConfigOverride.xcconfig; sourceTree = ""; }; + 29AC4F67277F8A2100766404 /* CarbSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbSource.swift; sourceTree = ""; }; 2AD22C985B79A2F0D2EA3D9D /* PumpConfigRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpConfigRootView.swift; sourceTree = ""; }; 2F2A13DF0EDEEEDC4106AA2A /* NightscoutConfigDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NightscoutConfigDataFlow.swift; sourceTree = ""; }; 3260468377DA9DB4DEE9AF6D /* NotificationsConfigDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NotificationsConfigDataFlow.swift; sourceTree = ""; }; @@ -1201,6 +1204,7 @@ children = ( 38F3783A2613555C009DB701 /* Config.xcconfig */, 3818AA42274BBC1100843DB3 /* ConfigOverride.xcconfig */, + 29AC4F65277F48C100766404 /* ConfigOverride.xcconfig */, 388E595A25AD948C0019842D /* FreeAPS */, 38FCF3EE25E9028E0078B0D1 /* FreeAPSTests */, 3818AA44274C229000843DB3 /* Packages */, @@ -1310,6 +1314,7 @@ 38A0363A25ECF07E00FCBB52 /* GlucoseStorage.swift */, 38FCF3FC25E997A80078B0D1 /* PumpHistoryStorage.swift */, 38F3B2EE25ED8E2A005C48AA /* TempTargetsStorage.swift */, + 29AC4F67277F8A2100766404 /* CarbSource.swift */, ); path = Storage; sourceTree = ""; @@ -2182,6 +2187,7 @@ 38E98A2525F52C9300C0CED0 /* IssueReporter.swift in Sources */, 3811DEB025C9D88300A708ED /* BaseKeychain.swift in Sources */, 3811DE4325C9D4A100A708ED /* SettingsProvider.swift in Sources */, + 29AC4F68277F8A2100766404 /* CarbSource.swift in Sources */, 45252C95D220E796FDB3B022 /* ConfigEditorDataFlow.swift in Sources */, 3871F38725ED661C0013ECB5 /* Suggestion.swift in Sources */, 38C4D33A25E9A1ED00D30B77 /* NSObject+AssociatedValues.swift in Sources */, diff --git a/FreeAPS/Sources/APS/FetchTreatmentsManager.swift b/FreeAPS/Sources/APS/FetchTreatmentsManager.swift index 54d55eaed..686242b65 100644 --- a/FreeAPS/Sources/APS/FetchTreatmentsManager.swift +++ b/FreeAPS/Sources/APS/FetchTreatmentsManager.swift @@ -23,16 +23,18 @@ final class BaseFetchTreatmentsManager: FetchTreatmentsManager, Injectable { private func subscribe() { timer.publisher .receive(on: processQueue) - .flatMap { _ -> AnyPublisher<([CarbsEntry], [TempTarget]), Never> in + .flatMap { _ -> AnyPublisher<([CarbsEntry], [TempTarget], [CarbsEntry]), Never> in debug(.nightscout, "FetchTreatmentsManager heartbeat") debug(.nightscout, "Start fetching carbs and temptargets") - return Publishers.CombineLatest( + return Publishers.CombineLatest3( self.nightscoutManager.fetchCarbs(), - self.nightscoutManager.fetchTempTargets() + self.nightscoutManager.fetchTempTargets(), + self.healthKitManager.fetch() ).eraseToAnyPublisher() } - .sink { carbs, targets in - let filteredCarbs = carbs.filter { !($0.enteredBy?.contains(CarbsEntry.manual) ?? false) } + .sink { carbs, targets, carbsFromHealth in + let allCarbs = carbs + carbsFromHealth + let filteredCarbs = allCarbs.filter { !($0.enteredBy?.contains(CarbsEntry.manual) ?? false) } if filteredCarbs.isNotEmpty { self.carbsStorage.storeCarbs(filteredCarbs) self.healthKitManager.saveIfNeeded(carbs: filteredCarbs) diff --git a/FreeAPS/Sources/APS/Storage/CarbSource.swift b/FreeAPS/Sources/APS/Storage/CarbSource.swift new file mode 100644 index 000000000..30e38ea97 --- /dev/null +++ b/FreeAPS/Sources/APS/Storage/CarbSource.swift @@ -0,0 +1,5 @@ +import Combine + +protocol CarbSource { + func fetch() -> AnyPublisher<[CarbsEntry], Never> +} diff --git a/FreeAPS/Sources/APS/Storage/CarbsStorage.swift b/FreeAPS/Sources/APS/Storage/CarbsStorage.swift index e234ed554..dae7f0fcd 100644 --- a/FreeAPS/Sources/APS/Storage/CarbsStorage.swift +++ b/FreeAPS/Sources/APS/Storage/CarbsStorage.swift @@ -36,45 +36,48 @@ final class BaseCarbsStorage: CarbsStorage, Injectable { .sorted { $0.createdAt > $1.createdAt } ?? [] storage.save(Array(uniqEvents), as: file) } - broadcaster.notify(CarbsObserver.self, on: processQueue) { - $0.carbsDidUpdate(uniqEvents) + + DispatchQueue.main.async { + self.broadcaster.notify(CarbsObserver.self, on: .main) { + $0.carbsDidUpdate(uniqEvents) + } } } } - + func removeCarbs(byDateCollection dates: [Date]) { - processQueue.sync { - let file = OpenAPS.Monitor.carbHistory - self.storage.transaction { storage in - let CarbInStorage = storage.retrieve(file, as: [CarbsEntry].self) - let filteredCarb = CarbInStorage?.filter { !ids.contains($0.id) } ?? [] - storage.save(filteredCarb, as: file) + processQueue.sync { + let file = OpenAPS.Monitor.carbHistory + self.storage.transaction { storage in + let CarbInStorage = storage.retrieve(file, as: [CarbsEntry].self) + let filteredCarb = CarbInStorage?.filter { !dates.contains($0.createdAt) } ?? [] + storage.save(filteredCarb, as: file) - DispatchQueue.main.async { - self.broadcaster.notify(CarbsObserver.self, on: .main) { - $0.carbsDidUpdate(filteredCarb.reversed()) - } - } - } - } - } + DispatchQueue.main.async { + self.broadcaster.notify(CarbsObserver.self, on: .main) { + $0.carbsDidUpdate(filteredCarb.reversed()) + } + } + } + } + } - func removeCarbs(byDate date: Date) { - processQueue.sync { - let file = OpenAPS.Monitor.carbHistory - self.storage.transaction { storage in - let CarbInStorage = storage.retrieve(file, as: [CarbsEntry].self) - let filteredCarb = CarbInStorage?.filter { $0.id != id } ?? [] - storage.save(filteredCarb, as: file) + func removeCarbs(byDate date: Date) { + processQueue.sync { + let file = OpenAPS.Monitor.carbHistory + self.storage.transaction { storage in + let CarbInStorage = storage.retrieve(file, as: [CarbsEntry].self) + let filteredCarb = CarbInStorage?.filter { $0.createdAt != date } ?? [] + storage.save(filteredCarb, as: file) - DispatchQueue.main.async { - self.broadcaster.notify(CarbsObserver.self, on: .main) { - $0.carbsDidUpdate((filteredCarb.reversed()) - } - } - } - } - } + DispatchQueue.main.async { + self.broadcaster.notify(CarbsObserver.self, on: .main) { + $0.carbsDidUpdate(filteredCarb.reversed()) + } + } + } + } + } func syncDate() -> Date { Date().addingTimeInterval(-1.days.timeInterval) diff --git a/FreeAPS/Sources/Models/CarbsEntry.swift b/FreeAPS/Sources/Models/CarbsEntry.swift index 9c4033c4e..17156859a 100644 --- a/FreeAPS/Sources/Models/CarbsEntry.swift +++ b/FreeAPS/Sources/Models/CarbsEntry.swift @@ -1,15 +1,15 @@ import Foundation struct CarbsEntry: JSON, Equatable, Hashable { - let createdAt: Date - let carbs: Decimal - let enteredBy: String? - var _id = UUID().uuidString var id: String { _id } - + + let createdAt: Date + let carbs: Decimal + let enteredBy: String? + static let manual = "freeaps-x" static func == (lhs: CarbsEntry, rhs: CarbsEntry) -> Bool { diff --git a/FreeAPS/Sources/Models/HealthKitSample.swift b/FreeAPS/Sources/Models/HealthKitSample.swift index b7c18ae4a..57368f7df 100644 --- a/FreeAPS/Sources/Models/HealthKitSample.swift +++ b/FreeAPS/Sources/Models/HealthKitSample.swift @@ -3,8 +3,8 @@ import Foundation struct HealthKitSample: JSON, Hashable, Equatable { var healthKitId: String var date: Date - var glucose: Int - var carb: Int + var glucose: Int? + var carb: Decimal? static func == (lhs: HealthKitSample, rhs: HealthKitSample) -> Bool { lhs.healthKitId == rhs.healthKitId diff --git a/FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift b/FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift index baf887600..9b0ca8c21 100644 --- a/FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift +++ b/FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift @@ -3,7 +3,7 @@ import Foundation import HealthKit import Swinject -protocol HealthKitManager: GlucoseSource, CarbsEntry { +protocol HealthKitManager: GlucoseSource, CarbSource { /// Check all needed permissions /// Return false if one or more permissions are deny or not choosen var areAllowAllPermissions: Bool { get } @@ -13,6 +13,7 @@ protocol HealthKitManager: GlucoseSource, CarbsEntry { func requestPermission(completion: ((Bool, Error?) -> Void)?) /// Save blood glucose to Health store (dublicate of bg will ignore) func saveIfNeeded(bloodGlucose: [BloodGlucose]) + /// Save carb to Health store (duplicates will be ignored) func saveIfNeeded(carbs: [CarbsEntry]) /// Create observer for data passing beetwen Health Store and FreeAPS func createObserver() @@ -20,18 +21,20 @@ protocol HealthKitManager: GlucoseSource, CarbsEntry { func enableBackgroundDelivery() /// Delete glucose with syncID func deleteGlucose(syncID: String) + /// Delete carb at specified date func deleteCarb(at: Date) } final class BaseHealthKitManager: HealthKitManager, Injectable { private enum Config { // unwraped HKObjects - static var permissions: Set { Set([healthBGObject].compactMap [healthCarbObject].compactMap { $0 }) } + static var permissions: Set { Set(healthObject.compactMap { $0 }) } - // link to object in HealthKit - static let healthBGObject = HKObjectType.quantityType(forIdentifier: .bloodGlucose) - - static let healthCarbObject = HKObjectType.quantityType(forIdentifier: .dietaryCarbohydrates) + // link to objects in HealthKit + static let healthObject = [ + HKObjectType.quantityType(forIdentifier: .bloodGlucose), + HKObjectType.quantityType(forIdentifier: .dietaryCarbohydrates) + ] // Meta-data key of FreeASPX data in HealthStore static let freeAPSMetaKey = "fromFreeAPSX" @@ -47,7 +50,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { // BG that will be return Publisher @SyncAccess @Persisted(key: "BaseHealthKitManager.newGlucose") private var newGlucose: [BloodGlucose] = [] - + @SyncAccess @Persisted(key: "BaseHealthKitManager.newCarb") private var newCarb: [CarbsEntry] = [] // last anchor for HKAnchoredQuery @@ -93,11 +96,10 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { return NSCompoundPredicate(andPredicateWithSubpredicates: [predicateByStartDate, predicateByMeta]) } - init(resolver: Resolver) { injectServices(resolver) guard isAvailableOnCurrentDevice, - Config.healthBGObject != nil else { return } + !Config.healthObject.isEmpty else { return } createObserver() enableBackgroundDelivery() debug(.service, "HealthKitManager did create") @@ -108,7 +110,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { } func checkAvailabilitySaveBG() -> Bool { - Config.healthBGObject.map { checkAvailabilitySave(objectTypeToHealthStore: $0) } ?? false + Config.healthObject[0].map { checkAvailabilitySave(objectTypeToHealthStore: $0) } ?? false } func requestPermission(completion: ((Bool, Error?) -> Void)? = nil) { @@ -128,7 +130,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { func saveIfNeeded(bloodGlucose: [BloodGlucose]) { guard settingsManager.settings.useAppleHealth, - let sampleType = Config.healthBGObject, + let sampleType = Config.healthObject[0], checkAvailabilitySave(objectTypeToHealthStore: sampleType), bloodGlucose.isNotEmpty else { return } @@ -161,15 +163,50 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { .store(in: &lifetime) } + func saveIfNeeded(carbs: [CarbsEntry]) { + guard settingsManager.settings.useAppleHealth, + let sampleType = Config.healthObject[1], + checkAvailabilitySave(objectTypeToHealthStore: sampleType), + carbs.isNotEmpty + else { return } + + func save(samples: [HKSample]) { + let sampleIDs = samples.compactMap(\.syncIdentifier) + let samplesToSave = carbs + .filter { !sampleIDs.contains($0.id) } + .map { + HKQuantitySample( + type: sampleType, + quantity: HKQuantity(unit: .gram(), doubleValue: Double($0.carbs)), + start: $0.createdAt, + end: $0.createdAt, + metadata: [ + HKMetadataKeyExternalUUID: $0.id, + HKMetadataKeySyncIdentifier: $0.id, + HKMetadataKeySyncVersion: 1, + Config.freeAPSMetaKey: true + ] + ) + } + + healthKitStore.save(samplesToSave) { _, _ in } + } + + loadSamplesFromHealth(sampleType: sampleType, withIDs: carbs.map(\.id)) + .receive(on: processQueue) + .sink(receiveValue: save) + .store(in: &lifetime) + } + func createObserver() { guard settingsManager.settings.useAppleHealth else { return } - guard let bgType = Config.healthBGObject else { + guard let bgType = Config.healthObject[0] else { warning(.service, "Can not create HealthKit Observer, because unable to get the Blood Glucose type") return } - - guard let carbType = Config.healthCarbObject else { + + guard let carbType = Config.healthObject[1] else { warning(.service, "Can not create HealthKit Observer, because unable to get the Carb type") return } @@ -187,7 +224,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { self.healthKitStore.execute(incrementQuery) } } - + let carbQuery = HKObserverQuery(sampleType: carbType, predicate: nil) { [weak self] _, _, observerError in guard let self = self else { return } debug(.service, "Execute HelathKit observer query for loading increment samples") @@ -201,7 +238,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { self.healthKitStore.execute(incrementQuery) } } - + healthKitStore.execute(glucoseQuery) healthKitStore.execute(carbQuery) debug(.service, "Create Observer for Blood Glucose") @@ -212,7 +249,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { healthKitStore.disableAllBackgroundDelivery { _, _ in } return } - guard let bgType = Config.healthBGObject else { + guard let bgType = Config.healthObject[0] else { warning( .service, "Can not create background delivery, because unable to get the Blood Glucose type" @@ -253,7 +290,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { } private func getBloodGlucoseHKQuery(predicate: NSPredicate) -> HKQuery? { - guard let sampleType = Config.healthBGObject else { return nil } + guard let sampleType = Config.healthObject[0] else { return nil } let query = HKAnchoredObjectQuery( type: sampleType, @@ -277,9 +314,9 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { } return query } - + private func getCarbHKQuery(predicate: NSPredicate) -> HKQuery? { - guard let sampleType = Config.healthCarbObject else { return nil } + guard let sampleType = Config.healthObject[1] else { return nil } let query = HKAnchoredObjectQuery( type: sampleType, @@ -315,7 +352,8 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { return HealthKitSample( healthKitId: sample.uuid.uuidString, date: sample.startDate, - glucose: Int(round(sample.quantity.doubleValue(for: .milligramsPerDeciliter))) + glucose: Int(round(sample.quantity.doubleValue(for: .milligramsPerDeciliter))), + carb: nil ) } .map { sample in @@ -341,30 +379,33 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { "Current BloodGlucose.Type objects will be send from Publisher during fetch: \(String(describing: newGlucose))" ) } - + private func prepareCarbSamplesToPublisherFetch(_ samples: [HKQuantitySample]) { dispatchPrecondition(condition: .onQueue(processQueue)) debug(.service, "Start preparing samples: \(String(describing: samples))") newCarb += samples .compactMap { sample -> HealthKitSample? in - //let fromFAX = sample.metadata?[Config.freeAPSMetaKey] as? Bool ?? false - //guard !fromFAX else { return nil } - return HealthKitSample( + // let fromFAX = sample.metadata?[Config.freeAPSMetaKey] as? Bool ?? false + // let isCarb = sample. + // guard !fromFAX else { return nil } + HealthKitSample( healthKitId: sample.uuid.uuidString, date: sample.startDate, - carb: sample.quantity.doubleValue(for: .gram()) + glucose: nil, + carb: sample.quantity.doubleValue(for: .gram()).decimal ) } .map { sample in - CarbsEntry( + let cb = sample.carb as? Decimal ?? 0.0 + return CarbsEntry( _id: sample.healthKitId, - createdAt: Decimal(Int(sample.date.timeIntervalSince1970) * 1000), - carbs: sample.carb, + createdAt: sample.date, + carbs: cb, enteredBy: nil ) } - .filter { $0.dateString >= Date().addingTimeInterval(-1.days.timeInterval) } + .filter { $0.createdAt >= Date().addingTimeInterval(-1.days.timeInterval) } newCarb = newCarb.removeDublicates() @@ -408,7 +449,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { } .eraseToAnyPublisher() } - + func fetch() -> AnyPublisher<[CarbsEntry], Never> { Future { [weak self] promise in guard let self = self else { @@ -426,10 +467,10 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { // Remove old BGs self.newCarb = self.newCarb - .filter { $0.dateString >= Date().addingTimeInterval(-1.days.timeInterval) } - // Get actual BGs (beetwen Date() - 1 day and Date()) + .filter { $0.createdAt >= Date().addingTimeInterval(-1.days.timeInterval) } + // Get actual carbs (beetwen Date() - 1 day and Date()) let actualCarb = self.newCarb - .filter { $0.dateString <= Date() } + .filter { $0.createdAt <= Date() } // Update newCarb self.newCarb = self.newCarb .filter { !actualCarb.contains($0) } @@ -446,7 +487,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { func deleteGlucose(syncID: String) { guard settingsManager.settings.useAppleHealth, - let sampleType = Config.healthBGObject, + let sampleType = Config.healthObject[0], checkAvailabilitySave(objectTypeToHealthStore: sampleType) else { return } @@ -463,10 +504,10 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { } } } - + func deleteCarb(at: Date) { guard settingsManager.settings.useAppleHealth, - let sampleType = Config.healthCarbObject, + let sampleType = Config.healthObject[1], checkAvailabilitySave(objectTypeToHealthStore: sampleType) else { return } @@ -479,7 +520,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { 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) + warning(.service, "Cannot delete sample with date: \(at)", error: error) } } } From 340178376994bbb47e0f8ced67b9a68bef13af23 Mon Sep 17 00:00:00 2001 From: Shawn Date: Sun, 2 Jan 2022 14:44:21 -0500 Subject: [PATCH 3/8] Create process queue for carbs instead of overloading glucose queue. Properly check permissions. Enable background delivery. --- .../HealthKit/HealthKitStateModel.swift | 7 +- .../Services/HealthKit/HealthKitManager.swift | 80 ++++++++++++------- 2 files changed, 58 insertions(+), 29 deletions(-) diff --git a/FreeAPS/Sources/Modules/HealthKit/HealthKitStateModel.swift b/FreeAPS/Sources/Modules/HealthKit/HealthKitStateModel.swift index fa23644b2..3eaa4c133 100644 --- a/FreeAPS/Sources/Modules/HealthKit/HealthKitStateModel.swift +++ b/FreeAPS/Sources/Modules/HealthKit/HealthKitStateModel.swift @@ -25,7 +25,10 @@ extension AppleHealthKit { self.healthKitManager.requestPermission { ok, error in DispatchQueue.main.async { - self.needShowInformationTextForSetPermissions = !self.healthKitManager.checkAvailabilitySaveBG() + self.needShowInformationTextForSetPermissions = !( + self.healthKitManager.checkAvailabilitySaveBG() && self + .healthKitManager.checkAvailabilitySaveCarb() + ) } guard ok, error == nil else { @@ -33,7 +36,7 @@ extension AppleHealthKit { return } - debug(.service, "Permission granted HealthKitManager") + debug(.service, "Permission granted HealthKitManager") self.healthKitManager.createObserver() self.healthKitManager.enableBackgroundDelivery() diff --git a/FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift b/FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift index 9b0ca8c21..58f1a7087 100644 --- a/FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift +++ b/FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift @@ -9,6 +9,8 @@ protocol HealthKitManager: GlucoseSource, CarbSource { var areAllowAllPermissions: Bool { get } /// Check availability to save data of BG type to Health store func checkAvailabilitySaveBG() -> Bool + /// Check availability to save data of Carb type to Health store + func checkAvailabilitySaveCarb() -> Bool /// Requests user to give permissions on using HealthKit func requestPermission(completion: ((Bool, Error?) -> Void)?) /// Save blood glucose to Health store (dublicate of bg will ignore) @@ -45,7 +47,8 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { @Injected() private var healthKitStore: HKHealthStore! @Injected() private var settingsManager: SettingsManager! - private let processQueue = DispatchQueue(label: "BaseHealthKitManager.processQueue") + private let glucoseProcessQueue = DispatchQueue(label: "BaseHealthKitManager.glucoseProcessQueue") + private let carbProcessQueue = DispatchQueue(label: "BaseHealthKitManager.carbProcessQueue") private var lifetime = Lifetime() // BG that will be return Publisher @@ -113,6 +116,10 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { Config.healthObject[0].map { checkAvailabilitySave(objectTypeToHealthStore: $0) } ?? false } + func checkAvailabilitySaveCarb() -> Bool { + Config.healthObject[1].map { checkAvailabilitySave(objectTypeToHealthStore: $0) } ?? false + } + func requestPermission(completion: ((Bool, Error?) -> Void)? = nil) { guard isAvailableOnCurrentDevice else { completion?(false, HKError.notAvailableOnCurrentDevice) @@ -158,7 +165,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { } loadSamplesFromHealth(sampleType: sampleType, withIDs: bloodGlucose.map(\.id)) - .receive(on: processQueue) + .receive(on: glucoseProcessQueue) .sink(receiveValue: save) .store(in: &lifetime) } @@ -193,7 +200,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { } loadSamplesFromHealth(sampleType: sampleType, withIDs: carbs.map(\.id)) - .receive(on: processQueue) + .receive(on: carbProcessQueue) .sink(receiveValue: save) .store(in: &lifetime) } @@ -234,7 +241,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { } if let incrementQuery = self.getCarbHKQuery(predicate: self.loadValuePredicate) { - debug(.service, "Create increment query") + debug(.service, "Create carb increment query") self.healthKitStore.execute(incrementQuery) } } @@ -257,12 +264,28 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { return } + guard let carbType = Config.healthObject[1] else { + warning( + .service, + "Can not create background delivery, because unable to get the Carb type" + ) + return + } + healthKitStore.enableBackgroundDelivery(for: bgType, frequency: .immediate) { status, error in guard error == nil else { - warning(.service, "Can not enable background delivery", error: error) + warning(.service, "Can not enable bg background delivery", error: error) return } - debug(.service, "Background delivery status is \(status)") + debug(.service, "Background delivery bg status is \(status)") + } + + healthKitStore.enableBackgroundDelivery(for: carbType, frequency: .immediate) { status, error in + guard error == nil else { + warning(.service, "Can not enable carb background delivery", error: error) + return + } + debug(.service, "Background delivery carb status is \(status)") } } @@ -299,16 +322,16 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { limit: HKObjectQueryNoLimit ) { [weak self] _, addedObjects, _, anchor, _ in guard let self = self else { return } - self.processQueue.async { - debug(.service, "AnchoredQuery did execute") + self.glucoseProcessQueue.async { + debug(.service, "AnchoredQuery for glucose did execute") self.lastQueryAnchor = anchor // Added objects - if let carbSamples = addedObjects as? [HKQuantitySample], - carbSamples.isNotEmpty + if let bgSamples = addedObjects as? [HKQuantitySample], + bgSamples.isNotEmpty { - self.prepareBGSamplesToPublisherFetch(carbSamples) + self.prepareBGSamplesToPublisherFetch(bgSamples) } } } @@ -325,16 +348,17 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { limit: HKObjectQueryNoLimit ) { [weak self] _, addedObjects, _, anchor, _ in guard let self = self else { return } - self.processQueue.async { - debug(.service, "AnchoredQuery did execute") + self.carbProcessQueue.async { + debug(.service, "AnchoredQuery for carbs did execute") self.lastQueryAnchor = anchor // Added objects - if let bgSamples = addedObjects as? [HKQuantitySample], - bgSamples.isNotEmpty + debug(.service, "getCarbHKQuery: \(String(describing: addedObjects))") + if let carbSamples = addedObjects as? [HKQuantitySample], + carbSamples.isNotEmpty { - self.prepareCarbSamplesToPublisherFetch(bgSamples) + self.prepareCarbSamplesToPublisherFetch(carbSamples) } } } @@ -342,7 +366,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { } private func prepareBGSamplesToPublisherFetch(_ samples: [HKQuantitySample]) { - dispatchPrecondition(condition: .onQueue(processQueue)) + dispatchPrecondition(condition: .onQueue(glucoseProcessQueue)) debug(.service, "Start preparing samples: \(String(describing: samples))") newGlucose += samples @@ -381,7 +405,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { } private func prepareCarbSamplesToPublisherFetch(_ samples: [HKQuantitySample]) { - dispatchPrecondition(condition: .onQueue(processQueue)) + dispatchPrecondition(condition: .onQueue(carbProcessQueue)) debug(.service, "Start preparing samples: \(String(describing: samples))") newCarb += samples @@ -422,10 +446,10 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { return } - self.processQueue.async { - debug(.service, "Start fetching HealthKitManager") + self.glucoseProcessQueue.async { + debug(.service, "Start fetching HealthKitManager Glucose") guard self.settingsManager.settings.useAppleHealth else { - debug(.service, "HealthKitManager cant return any data, because useAppleHealth option is disable") + debug(.service, "HealthKitManager cant return any glucose data, because useAppleHealth option is disable") promise(.success([])) return } @@ -457,15 +481,17 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { return } - self.processQueue.async { - debug(.service, "Start fetching HealthKitManager") + self.carbProcessQueue.async { + debug(.service, "Start fetching HealthKitManager Carbs") guard self.settingsManager.settings.useAppleHealth else { - debug(.service, "HealthKitManager cant return any data, because useAppleHealth option is disable") + debug(.service, "HealthKitManager cant return any carb data, because useAppleHealth option is disable") promise(.success([])) return } - // Remove old BGs + debug(.service, "Old state of newCarb is \(self.newCarb)") + + // Remove old carbs self.newCarb = self.newCarb .filter { $0.createdAt >= Date().addingTimeInterval(-1.days.timeInterval) } // Get actual carbs (beetwen Date() - 1 day and Date()) @@ -491,7 +517,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { checkAvailabilitySave(objectTypeToHealthStore: sampleType) else { return } - processQueue.async { + glucoseProcessQueue.async { let predicate = HKQuery.predicateForObjects( withMetadataKey: HKMetadataKeySyncIdentifier, operatorType: .equalTo, @@ -511,7 +537,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { checkAvailabilitySave(objectTypeToHealthStore: sampleType) else { return } - processQueue.async { + carbProcessQueue.async { let predicate = HKQuery.predicateForObjects( withMetadataKey: HKMetadataKeySyncIdentifier, operatorType: .equalTo, From 14e4354812b5b09a038d6a3e5f4c1cbc01452496 Mon Sep 17 00:00:00 2001 From: Shawn Date: Fri, 7 Jan 2022 17:37:17 -0500 Subject: [PATCH 4/8] Split out carb/bg observers --- .../Sources/APS/FetchTreatmentsManager.swift | 10 +++-- .../Modules/DataTable/DataTableProvider.swift | 2 +- .../HealthKit/HealthKitStateModel.swift | 3 +- .../Services/HealthKit/HealthKitManager.swift | 43 +++++++++++-------- 4 files changed, 34 insertions(+), 24 deletions(-) diff --git a/FreeAPS/Sources/APS/FetchTreatmentsManager.swift b/FreeAPS/Sources/APS/FetchTreatmentsManager.swift index 686242b65..4b33ae81c 100644 --- a/FreeAPS/Sources/APS/FetchTreatmentsManager.swift +++ b/FreeAPS/Sources/APS/FetchTreatmentsManager.swift @@ -23,19 +23,21 @@ final class BaseFetchTreatmentsManager: FetchTreatmentsManager, Injectable { private func subscribe() { timer.publisher .receive(on: processQueue) - .flatMap { _ -> AnyPublisher<([CarbsEntry], [TempTarget], [CarbsEntry]), Never> in + .flatMap { _ -> AnyPublisher<(Date, [CarbsEntry], [TempTarget], [CarbsEntry]), Never> in debug(.nightscout, "FetchTreatmentsManager heartbeat") debug(.nightscout, "Start fetching carbs and temptargets") - return Publishers.CombineLatest3( + return Publishers.CombineLatest4( + Just(self.carbsStorage.syncDate()), self.nightscoutManager.fetchCarbs(), self.nightscoutManager.fetchTempTargets(), self.healthKitManager.fetch() ).eraseToAnyPublisher() } - .sink { carbs, targets, carbsFromHealth in + .sink { syncDate, carbs, targets, carbsFromHealth in let allCarbs = carbs + carbsFromHealth - let filteredCarbs = allCarbs.filter { !($0.enteredBy?.contains(CarbsEntry.manual) ?? false) } + let filteredCarbs = allCarbs.filter { $0.createdAt > syncDate } if filteredCarbs.isNotEmpty { + debug(.nightscout, "Filtered carbs: \(filteredCarbs)") self.carbsStorage.storeCarbs(filteredCarbs) self.healthKitManager.saveIfNeeded(carbs: filteredCarbs) } diff --git a/FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift b/FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift index baff67c1b..84ef9cb79 100644 --- a/FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift +++ b/FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift @@ -18,7 +18,7 @@ extension DataTable { } func carbs() -> [CarbsEntry] { - carbsStorage.recent() + carbsStorage.recent().sorted { $0.createdAt > $1.createdAt } } func deleteCarbs(at date: Date) { diff --git a/FreeAPS/Sources/Modules/HealthKit/HealthKitStateModel.swift b/FreeAPS/Sources/Modules/HealthKit/HealthKitStateModel.swift index 3eaa4c133..fd5018b13 100644 --- a/FreeAPS/Sources/Modules/HealthKit/HealthKitStateModel.swift +++ b/FreeAPS/Sources/Modules/HealthKit/HealthKitStateModel.swift @@ -38,7 +38,8 @@ extension AppleHealthKit { debug(.service, "Permission granted HealthKitManager") - self.healthKitManager.createObserver() + self.healthKitManager.createGlucoseObserver() + self.healthKitManager.createCarbObserver() self.healthKitManager.enableBackgroundDelivery() } } diff --git a/FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift b/FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift index 58f1a7087..cd4aa5b77 100644 --- a/FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift +++ b/FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift @@ -17,8 +17,10 @@ protocol HealthKitManager: GlucoseSource, CarbSource { func saveIfNeeded(bloodGlucose: [BloodGlucose]) /// Save carb to Health store (duplicates will be ignored) func saveIfNeeded(carbs: [CarbsEntry]) - /// Create observer for data passing beetwen Health Store and FreeAPS - func createObserver() + /// Create observer for glucose data passing beetwen Health Store and FreeAPS + func createGlucoseObserver() + /// Create observer for carb data passing beetwen Health Store and FreeAPS + func createCarbObserver() /// Enable background delivering objects from Apple Health to FreeAPS func enableBackgroundDelivery() /// Delete glucose with syncID @@ -103,7 +105,8 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { injectServices(resolver) guard isAvailableOnCurrentDevice, !Config.healthObject.isEmpty else { return } - createObserver() + createGlucoseObserver() + createCarbObserver() enableBackgroundDelivery() debug(.service, "HealthKitManager did create") } @@ -205,7 +208,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { .store(in: &lifetime) } - func createObserver() { + func createGlucoseObserver() { guard settingsManager.settings.useAppleHealth else { return } guard let bgType = Config.healthObject[0] else { @@ -213,11 +216,6 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { return } - guard let carbType = Config.healthObject[1] else { - warning(.service, "Can not create HealthKit Observer, because unable to get the Carb type") - return - } - let glucoseQuery = HKObserverQuery(sampleType: bgType, predicate: nil) { [weak self] _, _, observerError in guard let self = self else { return } debug(.service, "Execute HelathKit observer query for loading increment samples") @@ -232,6 +230,18 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { } } + healthKitStore.execute(glucoseQuery) + debug(.service, "Create Observer for Blood Glucose") + } + + func createCarbObserver() { + guard settingsManager.settings.useAppleHealth else { return } + + guard let carbType = Config.healthObject[1] else { + warning(.service, "Can not create HealthKit Observer, because unable to get the Carb type") + return + } + let carbQuery = HKObserverQuery(sampleType: carbType, predicate: nil) { [weak self] _, _, observerError in guard let self = self else { return } debug(.service, "Execute HelathKit observer query for loading increment samples") @@ -246,9 +256,8 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { } } - healthKitStore.execute(glucoseQuery) healthKitStore.execute(carbQuery) - debug(.service, "Create Observer for Blood Glucose") + debug(.service, "Create Observer for Carbohydrate") } func enableBackgroundDelivery() { @@ -410,10 +419,9 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { newCarb += samples .compactMap { sample -> HealthKitSample? in - // let fromFAX = sample.metadata?[Config.freeAPSMetaKey] as? Bool ?? false - // let isCarb = sample. - // guard !fromFAX else { return nil } - HealthKitSample( + let fromFAX = sample.metadata?[Config.freeAPSMetaKey] as? Bool ?? false + guard !fromFAX else { return nil } + return HealthKitSample( healthKitId: sample.uuid.uuidString, date: sample.startDate, glucose: nil, @@ -421,11 +429,10 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { ) } .map { sample in - let cb = sample.carb as? Decimal ?? 0.0 - return CarbsEntry( + CarbsEntry( _id: sample.healthKitId, createdAt: sample.date, - carbs: cb, + carbs: sample.carb as? Decimal ?? 0.0, enteredBy: nil ) } From dd0c30b29153b67b030037096d33d4138b07f76b Mon Sep 17 00:00:00 2001 From: Shawn Date: Tue, 11 Jan 2022 14:20:03 -0500 Subject: [PATCH 5/8] Fix delete. --- .../Sources/APS/Storage/CarbsStorage.swift | 40 ++----------------- FreeAPS/Sources/Models/CarbsEntry.swift | 7 +--- .../Modules/AddCarbs/AddCarbsStateModel.swift | 3 +- .../Modules/DataTable/DataTableProvider.swift | 6 ++- .../Services/HealthKit/HealthKitManager.swift | 12 +++--- .../Services/WatchManager/WatchManager.swift | 2 +- 6 files changed, 20 insertions(+), 50 deletions(-) diff --git a/FreeAPS/Sources/APS/Storage/CarbsStorage.swift b/FreeAPS/Sources/APS/Storage/CarbsStorage.swift index dae7f0fcd..77d62da05 100644 --- a/FreeAPS/Sources/APS/Storage/CarbsStorage.swift +++ b/FreeAPS/Sources/APS/Storage/CarbsStorage.swift @@ -11,8 +11,6 @@ protocol CarbsStorage { func syncDate() -> Date func recent() -> [CarbsEntry] func nightscoutTretmentsNotUploaded() -> [NigtscoutTreatment] - func removeCarbs(byDate date: Date) - func removeCarbs(byDateCollection dates: [Date]) func deleteCarbs(at date: Date) } @@ -34,6 +32,10 @@ final class BaseCarbsStorage: CarbsStorage, Injectable { uniqEvents = storage.retrieve(file, as: [CarbsEntry].self)? .filter { $0.createdAt.addingTimeInterval(1.days.timeInterval) > Date() } .sorted { $0.createdAt > $1.createdAt } ?? [] + debug( + .service, + "Storing file carbs: \(String(describing: uniqEvents))" + ) storage.save(Array(uniqEvents), as: file) } @@ -45,40 +47,6 @@ final class BaseCarbsStorage: CarbsStorage, Injectable { } } - func removeCarbs(byDateCollection dates: [Date]) { - processQueue.sync { - let file = OpenAPS.Monitor.carbHistory - self.storage.transaction { storage in - let CarbInStorage = storage.retrieve(file, as: [CarbsEntry].self) - let filteredCarb = CarbInStorage?.filter { !dates.contains($0.createdAt) } ?? [] - storage.save(filteredCarb, as: file) - - DispatchQueue.main.async { - self.broadcaster.notify(CarbsObserver.self, on: .main) { - $0.carbsDidUpdate(filteredCarb.reversed()) - } - } - } - } - } - - func removeCarbs(byDate date: Date) { - processQueue.sync { - let file = OpenAPS.Monitor.carbHistory - self.storage.transaction { storage in - let CarbInStorage = storage.retrieve(file, as: [CarbsEntry].self) - let filteredCarb = CarbInStorage?.filter { $0.createdAt != date } ?? [] - storage.save(filteredCarb, as: file) - - DispatchQueue.main.async { - self.broadcaster.notify(CarbsObserver.self, on: .main) { - $0.carbsDidUpdate(filteredCarb.reversed()) - } - } - } - } - } - func syncDate() -> Date { Date().addingTimeInterval(-1.days.timeInterval) } diff --git a/FreeAPS/Sources/Models/CarbsEntry.swift b/FreeAPS/Sources/Models/CarbsEntry.swift index 17156859a..b8acd7558 100644 --- a/FreeAPS/Sources/Models/CarbsEntry.swift +++ b/FreeAPS/Sources/Models/CarbsEntry.swift @@ -1,11 +1,7 @@ import Foundation struct CarbsEntry: JSON, Equatable, Hashable { - var _id = UUID().uuidString - var id: String { - _id - } - + let id: String let createdAt: Date let carbs: Decimal let enteredBy: String? @@ -23,6 +19,7 @@ struct CarbsEntry: JSON, Equatable, Hashable { extension CarbsEntry { private enum CodingKeys: String, CodingKey { + case id case createdAt = "created_at" case carbs case enteredBy diff --git a/FreeAPS/Sources/Modules/AddCarbs/AddCarbsStateModel.swift b/FreeAPS/Sources/Modules/AddCarbs/AddCarbsStateModel.swift index 7bac8146c..d5fc63b87 100644 --- a/FreeAPS/Sources/Modules/AddCarbs/AddCarbsStateModel.swift +++ b/FreeAPS/Sources/Modules/AddCarbs/AddCarbsStateModel.swift @@ -4,6 +4,7 @@ extension AddCarbs { final class StateModel: BaseStateModel { @Injected() var carbsStorage: CarbsStorage! @Injected() var apsManager: APSManager! + @Published var ID: String = UUID().uuidString @Published var carbs: Decimal = 0 @Published var date = Date() @Published var carbsRequired: Decimal? @@ -19,7 +20,7 @@ extension AddCarbs { } carbsStorage.storeCarbs([ - CarbsEntry(createdAt: date, carbs: carbs, enteredBy: CarbsEntry.manual) + CarbsEntry(id: ID, createdAt: date, carbs: carbs, enteredBy: CarbsEntry.manual) ]) if settingsManager.settings.skipBolusScreenAfterCarbs { diff --git a/FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift b/FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift index 84ef9cb79..66027250f 100644 --- a/FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift +++ b/FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift @@ -22,8 +22,12 @@ extension DataTable { } func deleteCarbs(at date: Date) { + guard let healthCarb = carbsStorage.recent().first(where: { $0.createdAt == date }) else { + debug(.service, "Failed to delete carbs") + return + } nightscoutManager.deleteCarbs(at: date) - healthkitManager.deleteCarb(at: date) + healthkitManager.deleteCarb(syncID: healthCarb.id) } func glucose() -> [BloodGlucose] { diff --git a/FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift b/FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift index cd4aa5b77..95a328946 100644 --- a/FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift +++ b/FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift @@ -26,7 +26,7 @@ protocol HealthKitManager: GlucoseSource, CarbSource { /// Delete glucose with syncID func deleteGlucose(syncID: String) /// Delete carb at specified date - func deleteCarb(at: Date) + func deleteCarb(syncID: String) } final class BaseHealthKitManager: HealthKitManager, Injectable { @@ -430,10 +430,10 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { } .map { sample in CarbsEntry( - _id: sample.healthKitId, + id: sample.healthKitId, createdAt: sample.date, carbs: sample.carb as? Decimal ?? 0.0, - enteredBy: nil + enteredBy: CarbsEntry.manual ) } .filter { $0.createdAt >= Date().addingTimeInterval(-1.days.timeInterval) } @@ -538,7 +538,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { } } - func deleteCarb(at: Date) { + func deleteCarb(syncID: String) { guard settingsManager.settings.useAppleHealth, let sampleType = Config.healthObject[1], checkAvailabilitySave(objectTypeToHealthStore: sampleType) @@ -548,12 +548,12 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { let predicate = HKQuery.predicateForObjects( withMetadataKey: HKMetadataKeySyncIdentifier, operatorType: .equalTo, - value: at + value: syncID ) self.healthKitStore.deleteObjects(of: sampleType, predicate: predicate) { _, _, error in guard let error = error else { return } - warning(.service, "Cannot delete sample with date: \(at)", error: error) + warning(.service, "Cannot delete sample with syncID: \(syncID)", error: error) } } } diff --git a/FreeAPS/Sources/Services/WatchManager/WatchManager.swift b/FreeAPS/Sources/Services/WatchManager/WatchManager.swift index e5b0abeb2..bfb2d093d 100644 --- a/FreeAPS/Sources/Services/WatchManager/WatchManager.swift +++ b/FreeAPS/Sources/Services/WatchManager/WatchManager.swift @@ -217,7 +217,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 { From af9ec3c3dbf719f1c07aa72faa151e31c40400d1 Mon Sep 17 00:00:00 2001 From: Shawn Date: Wed, 19 Jan 2022 07:22:53 -0500 Subject: [PATCH 6/8] Add carbs from FAPX to health --- FreeAPS/Sources/Modules/AddCarbs/AddCarbsStateModel.swift | 7 +++++-- FreeAPS/Sources/Services/WatchManager/WatchManager.swift | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/FreeAPS/Sources/Modules/AddCarbs/AddCarbsStateModel.swift b/FreeAPS/Sources/Modules/AddCarbs/AddCarbsStateModel.swift index d5fc63b87..dd07239e3 100644 --- a/FreeAPS/Sources/Modules/AddCarbs/AddCarbsStateModel.swift +++ b/FreeAPS/Sources/Modules/AddCarbs/AddCarbsStateModel.swift @@ -4,6 +4,7 @@ extension AddCarbs { final class StateModel: BaseStateModel { @Injected() var carbsStorage: CarbsStorage! @Injected() var apsManager: APSManager! + @Injected() var healthKitManager: HealthKitManager! @Published var ID: String = UUID().uuidString @Published var carbs: Decimal = 0 @Published var date = Date() @@ -19,9 +20,11 @@ extension AddCarbs { return } - carbsStorage.storeCarbs([ + let carbArray = [ CarbsEntry(id: ID, createdAt: date, carbs: carbs, enteredBy: CarbsEntry.manual) - ]) + ] + carbsStorage.storeCarbs(carbArray) + healthKitManager.saveIfNeeded(carbs: carbArray) if settingsManager.settings.skipBolusScreenAfterCarbs { apsManager.determineBasalSync() diff --git a/FreeAPS/Sources/Services/WatchManager/WatchManager.swift b/FreeAPS/Sources/Services/WatchManager/WatchManager.swift index bfb2d093d..ab8144fff 100644 --- a/FreeAPS/Sources/Services/WatchManager/WatchManager.swift +++ b/FreeAPS/Sources/Services/WatchManager/WatchManager.swift @@ -16,6 +16,7 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable { @Injected() private var storage: FileStorage! @Injected() private var carbsStorage: CarbsStorage! @Injected() private var tempTargetsStorage: TempTargetsStorage! + @Injected() var healthKitManager: HealthKitManager! private var lifetime = Lifetime() @@ -216,9 +217,11 @@ extension BaseWatchManager: WCSessionDelegate { debug(.service, "WCSession got message with reply handler: \(message)") if let carbs = message["carbs"] as? Double, carbs > 0 { - carbsStorage.storeCarbs([ + let carbArray = [ CarbsEntry(id: UUID().uuidString, createdAt: Date(), carbs: Decimal(carbs), enteredBy: CarbsEntry.manual) - ]) + ] + carbsStorage.storeCarbs(carbArray) + healthKitManager.saveIfNeeded(carbs: carbArray) if settingsManager.settings.skipBolusScreenAfterCarbs { apsManager.determineBasalSync() From 40a8d2f226bdeec56546e68cb9c1ec0659e5aadf Mon Sep 17 00:00:00 2001 From: Shawn Date: Fri, 28 Jan 2022 17:03:33 -0500 Subject: [PATCH 7/8] PR Feedback --- FreeAPS.xcodeproj/project.pbxproj | 18 +++--- .../Sources/APS/FetchTreatmentsManager.swift | 17 +++--- FreeAPS/Sources/APS/Storage/CarbSource.swift | 2 +- FreeAPS/Sources/Models/CarbsEntry.swift | 1 + .../Services/HealthKit/HealthKitManager.swift | 56 +++++++++++-------- .../Services/Network/NightscoutManager.swift | 2 +- 6 files changed, 53 insertions(+), 43 deletions(-) diff --git a/FreeAPS.xcodeproj/project.pbxproj b/FreeAPS.xcodeproj/project.pbxproj index 793109546..c58f79557 100644 --- a/FreeAPS.xcodeproj/project.pbxproj +++ b/FreeAPS.xcodeproj/project.pbxproj @@ -483,7 +483,6 @@ 3811DEE725CA063400A708ED /* PersistedProperty.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PersistedProperty.swift; sourceTree = ""; }; 3811DF0125CA9FEA00A708ED /* Credentials.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Credentials.swift; sourceTree = ""; }; 3811DF0F25CAAAE200A708ED /* APSManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APSManager.swift; sourceTree = ""; }; - 3818AA42274BBC1100843DB3 /* ConfigOverride.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ConfigOverride.xcconfig; sourceTree = ""; }; 3818AA45274C229000843DB3 /* LibreTransmitter */ = {isa = PBXFileReference; lastKnownFileType = folder; name = LibreTransmitter; path = Dependencies/LibreTransmitter; sourceTree = ""; }; 3818AA49274C267000843DB3 /* CGMBLEKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CGMBLEKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3818AA4C274C26A300843DB3 /* LoopKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = LoopKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1203,7 +1202,6 @@ isa = PBXGroup; children = ( 38F3783A2613555C009DB701 /* Config.xcconfig */, - 3818AA42274BBC1100843DB3 /* ConfigOverride.xcconfig */, 29AC4F65277F48C100766404 /* ConfigOverride.xcconfig */, 388E595A25AD948C0019842D /* FreeAPS */, 38FCF3EE25E9028E0078B0D1 /* FreeAPSTests */, @@ -2559,7 +2557,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = "${DEVELOPER_TEAM}"; + DEVELOPMENT_TEAM = 4W28235M63; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = FreeAPS/Resources/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -2595,7 +2593,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = "${DEVELOPER_TEAM}"; + DEVELOPMENT_TEAM = 4W28235M63; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = FreeAPS/Resources/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -2631,7 +2629,7 @@ CODE_SIGN_ENTITLEMENTS = FreeAPSWatch/FreeAPSWatch.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(BUILD_VERSION)"; - DEVELOPMENT_TEAM = "${DEVELOPER_TEAM}"; + DEVELOPMENT_TEAM = 4W28235M63; GENERATE_INFOPLIST_FILE = YES; IBSC_MODULE = FreeAPSWatch_WatchKit_Extension; INFOPLIST_KEY_CFBundleDisplayName = "$(APP_DISPLAY_NAME)"; @@ -2661,7 +2659,7 @@ CODE_SIGN_ENTITLEMENTS = FreeAPSWatch/FreeAPSWatch.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(BUILD_VERSION)"; - DEVELOPMENT_TEAM = "${DEVELOPER_TEAM}"; + DEVELOPMENT_TEAM = 4W28235M63; GENERATE_INFOPLIST_FILE = YES; IBSC_MODULE = FreeAPSWatch_WatchKit_Extension; INFOPLIST_KEY_CFBundleDisplayName = "$(APP_DISPLAY_NAME)"; @@ -2690,7 +2688,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(BUILD_VERSION)"; DEVELOPMENT_ASSET_PATHS = "\"FreeAPSWatch WatchKit Extension/Preview Content\""; - DEVELOPMENT_TEAM = "${DEVELOPER_TEAM}"; + DEVELOPMENT_TEAM = 4W28235M63; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "FreeAPSWatch WatchKit Extension/Info.plist"; @@ -2726,7 +2724,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(BUILD_VERSION)"; DEVELOPMENT_ASSET_PATHS = "\"FreeAPSWatch WatchKit Extension/Preview Content\""; - DEVELOPMENT_TEAM = "${DEVELOPER_TEAM}"; + DEVELOPMENT_TEAM = 4W28235M63; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "FreeAPSWatch WatchKit Extension/Info.plist"; @@ -2756,7 +2754,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)"; + DEVELOPMENT_TEAM = 4W28235M63; INFOPLIST_FILE = FreeAPSTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.4; LD_RUNPATH_SEARCH_PATHS = ( @@ -2777,7 +2775,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)"; + DEVELOPMENT_TEAM = 4W28235M63; INFOPLIST_FILE = FreeAPSTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.4; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/FreeAPS/Sources/APS/FetchTreatmentsManager.swift b/FreeAPS/Sources/APS/FetchTreatmentsManager.swift index 4b33ae81c..549ca2a92 100644 --- a/FreeAPS/Sources/APS/FetchTreatmentsManager.swift +++ b/FreeAPS/Sources/APS/FetchTreatmentsManager.swift @@ -23,22 +23,25 @@ final class BaseFetchTreatmentsManager: FetchTreatmentsManager, Injectable { private func subscribe() { timer.publisher .receive(on: processQueue) - .flatMap { _ -> AnyPublisher<(Date, [CarbsEntry], [TempTarget], [CarbsEntry]), Never> in + .flatMap { _ -> AnyPublisher<([CarbsEntry], [TempTarget], [CarbsEntry]), Never> in debug(.nightscout, "FetchTreatmentsManager heartbeat") debug(.nightscout, "Start fetching carbs and temptargets") - return Publishers.CombineLatest4( - Just(self.carbsStorage.syncDate()), + return Publishers.CombineLatest3( self.nightscoutManager.fetchCarbs(), self.nightscoutManager.fetchTempTargets(), - self.healthKitManager.fetch() + self.healthKitManager.fetchCarbs() ).eraseToAnyPublisher() } - .sink { syncDate, carbs, targets, carbsFromHealth in + .sink { carbs, targets, carbsFromHealth in let allCarbs = carbs + carbsFromHealth - let filteredCarbs = allCarbs.filter { $0.createdAt > syncDate } + let since = self.carbsStorage.syncDate() + let filteredCarbs = allCarbs.filter { $0.createdAt > since } + let carbsForHealth = allCarbs.filter { !carbsFromHealth.contains($0) } + if filteredCarbs.isNotEmpty { - debug(.nightscout, "Filtered carbs: \(filteredCarbs)") self.carbsStorage.storeCarbs(filteredCarbs) + } + if carbsForHealth.isNotEmpty { self.healthKitManager.saveIfNeeded(carbs: filteredCarbs) } let filteredTargets = targets.filter { !($0.enteredBy?.contains(TempTarget.manual) ?? false) } diff --git a/FreeAPS/Sources/APS/Storage/CarbSource.swift b/FreeAPS/Sources/APS/Storage/CarbSource.swift index 30e38ea97..48c2c51c3 100644 --- a/FreeAPS/Sources/APS/Storage/CarbSource.swift +++ b/FreeAPS/Sources/APS/Storage/CarbSource.swift @@ -1,5 +1,5 @@ import Combine protocol CarbSource { - func fetch() -> AnyPublisher<[CarbsEntry], Never> + func fetchCarbs() -> AnyPublisher<[CarbsEntry], Never> } diff --git a/FreeAPS/Sources/Models/CarbsEntry.swift b/FreeAPS/Sources/Models/CarbsEntry.swift index b8acd7558..f22a705bc 100644 --- a/FreeAPS/Sources/Models/CarbsEntry.swift +++ b/FreeAPS/Sources/Models/CarbsEntry.swift @@ -7,6 +7,7 @@ struct CarbsEntry: JSON, Equatable, Hashable { let enteredBy: String? static let manual = "freeaps-x" + static let healthKit = "healthkit" static func == (lhs: CarbsEntry, rhs: CarbsEntry) -> Bool { lhs.createdAt == rhs.createdAt diff --git a/FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift b/FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift index 95a328946..5781fe7b6 100644 --- a/FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift +++ b/FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift @@ -32,10 +32,10 @@ protocol HealthKitManager: GlucoseSource, CarbSource { final class BaseHealthKitManager: HealthKitManager, Injectable { private enum Config { // unwraped HKObjects - static var permissions: Set { Set(healthObject.compactMap { $0 }) } + static var permissions: Set { Set(healthObjects.compactMap { $0 }) } // link to objects in HealthKit - static let healthObject = [ + static let healthObjects = [ HKObjectType.quantityType(forIdentifier: .bloodGlucose), HKObjectType.quantityType(forIdentifier: .dietaryCarbohydrates) ] @@ -44,8 +44,6 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { static let freeAPSMetaKey = "fromFreeAPSX" } - @Injected() private var glucoseStorage: GlucoseStorage! - @Injected() private var carbsStorage: CarbsStorage! @Injected() private var healthKitStore: HKHealthStore! @Injected() private var settingsManager: SettingsManager! @@ -59,7 +57,17 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { @SyncAccess @Persisted(key: "BaseHealthKitManager.newCarb") private var newCarb: [CarbsEntry] = [] // last anchor for HKAnchoredQuery - private var lastQueryAnchor: HKQueryAnchor? { + private var lastBloodGlucoseQueryAnchor: HKQueryAnchor? { + set { + persistedAnchor = try? NSKeyedArchiver.archivedData(withRootObject: newValue as Any, requiringSecureCoding: false) + } + get { + guard let data = persistedAnchor else { return nil } + return try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? HKQueryAnchor + } + } + + private var lastCarbQueryAnchor: HKQueryAnchor? { set { persistedAnchor = try? NSKeyedArchiver.archivedData(withRootObject: newValue as Any, requiringSecureCoding: false) } @@ -104,7 +112,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { init(resolver: Resolver) { injectServices(resolver) guard isAvailableOnCurrentDevice, - !Config.healthObject.isEmpty else { return } + !Config.healthObjects.isEmpty else { return } createGlucoseObserver() createCarbObserver() enableBackgroundDelivery() @@ -116,11 +124,11 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { } func checkAvailabilitySaveBG() -> Bool { - Config.healthObject[0].map { checkAvailabilitySave(objectTypeToHealthStore: $0) } ?? false + Config.healthObjects[0].map { checkAvailabilitySave(objectTypeToHealthStore: $0) } ?? false } func checkAvailabilitySaveCarb() -> Bool { - Config.healthObject[1].map { checkAvailabilitySave(objectTypeToHealthStore: $0) } ?? false + Config.healthObjects[1].map { checkAvailabilitySave(objectTypeToHealthStore: $0) } ?? false } func requestPermission(completion: ((Bool, Error?) -> Void)? = nil) { @@ -140,7 +148,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { func saveIfNeeded(bloodGlucose: [BloodGlucose]) { guard settingsManager.settings.useAppleHealth, - let sampleType = Config.healthObject[0], + let sampleType = Config.healthObjects[0], checkAvailabilitySave(objectTypeToHealthStore: sampleType), bloodGlucose.isNotEmpty else { return } @@ -175,7 +183,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { func saveIfNeeded(carbs: [CarbsEntry]) { guard settingsManager.settings.useAppleHealth, - let sampleType = Config.healthObject[1], + let sampleType = Config.healthObjects[1], checkAvailabilitySave(objectTypeToHealthStore: sampleType), carbs.isNotEmpty else { return } @@ -211,7 +219,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { func createGlucoseObserver() { guard settingsManager.settings.useAppleHealth else { return } - guard let bgType = Config.healthObject[0] else { + guard let bgType = Config.healthObjects[0] else { warning(.service, "Can not create HealthKit Observer, because unable to get the Blood Glucose type") return } @@ -237,7 +245,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { func createCarbObserver() { guard settingsManager.settings.useAppleHealth else { return } - guard let carbType = Config.healthObject[1] else { + guard let carbType = Config.healthObjects[1] else { warning(.service, "Can not create HealthKit Observer, because unable to get the Carb type") return } @@ -265,7 +273,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { healthKitStore.disableAllBackgroundDelivery { _, _ in } return } - guard let bgType = Config.healthObject[0] else { + guard let bgType = Config.healthObjects[0] else { warning( .service, "Can not create background delivery, because unable to get the Blood Glucose type" @@ -273,7 +281,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { return } - guard let carbType = Config.healthObject[1] else { + guard let carbType = Config.healthObjects[1] else { warning( .service, "Can not create background delivery, because unable to get the Carb type" @@ -322,19 +330,19 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { } private func getBloodGlucoseHKQuery(predicate: NSPredicate) -> HKQuery? { - guard let sampleType = Config.healthObject[0] else { return nil } + guard let sampleType = Config.healthObjects[0] else { return nil } let query = HKAnchoredObjectQuery( type: sampleType, predicate: predicate, - anchor: lastQueryAnchor, + anchor: lastBloodGlucoseQueryAnchor, limit: HKObjectQueryNoLimit ) { [weak self] _, addedObjects, _, anchor, _ in guard let self = self else { return } self.glucoseProcessQueue.async { debug(.service, "AnchoredQuery for glucose did execute") - self.lastQueryAnchor = anchor + self.lastBloodGlucoseQueryAnchor = anchor // Added objects if let bgSamples = addedObjects as? [HKQuantitySample], @@ -348,19 +356,19 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { } private func getCarbHKQuery(predicate: NSPredicate) -> HKQuery? { - guard let sampleType = Config.healthObject[1] else { return nil } + guard let sampleType = Config.healthObjects[1] else { return nil } let query = HKAnchoredObjectQuery( type: sampleType, predicate: predicate, - anchor: lastQueryAnchor, + anchor: lastCarbQueryAnchor, limit: HKObjectQueryNoLimit ) { [weak self] _, addedObjects, _, anchor, _ in guard let self = self else { return } self.carbProcessQueue.async { debug(.service, "AnchoredQuery for carbs did execute") - self.lastQueryAnchor = anchor + self.lastCarbQueryAnchor = anchor // Added objects debug(.service, "getCarbHKQuery: \(String(describing: addedObjects))") @@ -433,7 +441,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { id: sample.healthKitId, createdAt: sample.date, carbs: sample.carb as? Decimal ?? 0.0, - enteredBy: CarbsEntry.manual + enteredBy: CarbsEntry.healthKit ) } .filter { $0.createdAt >= Date().addingTimeInterval(-1.days.timeInterval) } @@ -481,7 +489,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { .eraseToAnyPublisher() } - func fetch() -> AnyPublisher<[CarbsEntry], Never> { + func fetchCarbs() -> AnyPublisher<[CarbsEntry], Never> { Future { [weak self] promise in guard let self = self else { promise(.success([])) @@ -520,7 +528,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { func deleteGlucose(syncID: String) { guard settingsManager.settings.useAppleHealth, - let sampleType = Config.healthObject[0], + let sampleType = Config.healthObjects[0], checkAvailabilitySave(objectTypeToHealthStore: sampleType) else { return } @@ -540,7 +548,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { func deleteCarb(syncID: String) { guard settingsManager.settings.useAppleHealth, - let sampleType = Config.healthObject[1], + let sampleType = Config.healthObjects[1], checkAvailabilitySave(objectTypeToHealthStore: sampleType) else { return } diff --git a/FreeAPS/Sources/Services/Network/NightscoutManager.swift b/FreeAPS/Sources/Services/Network/NightscoutManager.swift index c6b20767c..f40cd2d78 100644 --- a/FreeAPS/Sources/Services/Network/NightscoutManager.swift +++ b/FreeAPS/Sources/Services/Network/NightscoutManager.swift @@ -3,7 +3,7 @@ import Foundation import Swinject import UIKit -protocol NightscoutManager: GlucoseSource { +protocol NightscoutManager: GlucoseSource, CarbSource { func fetchGlucose(since date: Date) -> AnyPublisher<[BloodGlucose], Never> func fetchCarbs() -> AnyPublisher<[CarbsEntry], Never> func fetchTempTargets() -> AnyPublisher<[TempTarget], Never> From dae8954a6bdee56f3ebe5d72df974c3946cf0993 Mon Sep 17 00:00:00 2001 From: Shawn Date: Fri, 28 Jan 2022 17:11:13 -0500 Subject: [PATCH 8/8] Put in nightscout carb filter --- FreeAPS/Sources/APS/FetchTreatmentsManager.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/FreeAPS/Sources/APS/FetchTreatmentsManager.swift b/FreeAPS/Sources/APS/FetchTreatmentsManager.swift index 549ca2a92..7457871a2 100644 --- a/FreeAPS/Sources/APS/FetchTreatmentsManager.swift +++ b/FreeAPS/Sources/APS/FetchTreatmentsManager.swift @@ -33,8 +33,10 @@ final class BaseFetchTreatmentsManager: FetchTreatmentsManager, Injectable { ).eraseToAnyPublisher() } .sink { carbs, targets, carbsFromHealth in - let allCarbs = carbs + carbsFromHealth let since = self.carbsStorage.syncDate() + let nsCarbs = carbs.filter { !($0.enteredBy?.contains(CarbsEntry.manual) ?? false) } + + let allCarbs = nsCarbs + carbsFromHealth let filteredCarbs = allCarbs.filter { $0.createdAt > since } let carbsForHealth = allCarbs.filter { !carbsFromHealth.contains($0) }