HealthKitManager.swift 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708
  1. import Combine
  2. import CoreData
  3. import Foundation
  4. import HealthKit
  5. import LoopKit
  6. import LoopKitUI
  7. import Swinject
  8. protocol HealthKitManager: GlucoseSource {
  9. /// Check all needed permissions
  10. /// Return false if one or more permissions are deny or not choosen
  11. var areAllowAllPermissions: Bool { get }
  12. /// Check availability to save data of BG type to Health store
  13. func checkAvailabilitySaveBG() -> Bool
  14. /// Requests user to give permissions on using HealthKit
  15. func requestPermission() async throws -> Bool
  16. /// Save blood glucose to Health store
  17. func uploadGlucose() async
  18. /// Save carbs to Health store
  19. func uploadCarbs() async
  20. /// Save Insulin to Health store
  21. func uploadInsulin() async
  22. /// Create observer for data passing beetwen Health Store and Trio
  23. func createBGObserver()
  24. /// Enable background delivering objects from Apple Health to Trio
  25. func enableBackgroundDelivery()
  26. /// Delete glucose with syncID
  27. func deleteGlucose(syncID: String) async
  28. /// delete carbs with syncID
  29. func deleteCarbs(syncID: String, fpuID: String)
  30. /// delete insulin with syncID
  31. func deleteInsulin(syncID: String)
  32. }
  33. final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsStoredDelegate, PumpHistoryDelegate {
  34. private enum Config {
  35. // unwraped HKObjects
  36. static var readPermissions: Set<HKSampleType> {
  37. Set([healthBGObject].compactMap { $0 }) }
  38. static var writePermissions: Set<HKSampleType> {
  39. Set([healthBGObject, healthCarbObject, healthFatObject, healthProteinObject, healthInsulinObject].compactMap { $0 }) }
  40. // link to object in HealthKit
  41. static let healthBGObject = HKObjectType.quantityType(forIdentifier: .bloodGlucose)
  42. static let healthCarbObject = HKObjectType.quantityType(forIdentifier: .dietaryCarbohydrates)
  43. static let healthFatObject = HKObjectType.quantityType(forIdentifier: .dietaryFatTotal)
  44. static let healthProteinObject = HKObjectType.quantityType(forIdentifier: .dietaryProtein)
  45. static let healthInsulinObject = HKObjectType.quantityType(forIdentifier: .insulinDelivery)
  46. // Meta-data key of FreeASPX data in HealthStore
  47. static let freeAPSMetaKey = "From Trio"
  48. }
  49. @Injected() private var glucoseStorage: GlucoseStorage!
  50. @Injected() private var healthKitStore: HKHealthStore!
  51. @Injected() private var settingsManager: SettingsManager!
  52. @Injected() private var broadcaster: Broadcaster!
  53. @Injected() var carbsStorage: CarbsStorage!
  54. @Injected() var pumpHistoryStorage: PumpHistoryStorage!
  55. private var backgroundContext = CoreDataStack.shared.newTaskContext()
  56. func carbsStorageHasUpdatedCarbs(_: BaseCarbsStorage) {
  57. Task.detached { [weak self] in
  58. guard let self = self else { return }
  59. await self.uploadCarbs()
  60. }
  61. }
  62. func pumpHistoryHasUpdated(_: BasePumpHistoryStorage) {
  63. Task.detached { [weak self] in
  64. guard let self = self else { return }
  65. await self.uploadInsulin()
  66. }
  67. }
  68. private let processQueue = DispatchQueue(label: "BaseHealthKitManager.processQueue")
  69. private var lifetime = Lifetime()
  70. // BG that will be return Publisher
  71. @SyncAccess @Persisted(key: "BaseHealthKitManager.newGlucose") private var newGlucose: [BloodGlucose] = []
  72. // last anchor for HKAnchoredQuery
  73. private var lastBloodGlucoseQueryAnchor: HKQueryAnchor? {
  74. set {
  75. persistedBGAnchor = try? NSKeyedArchiver.archivedData(withRootObject: newValue as Any, requiringSecureCoding: false)
  76. }
  77. get {
  78. guard let data = persistedBGAnchor else { return nil }
  79. return try? NSKeyedUnarchiver.unarchivedObject(ofClass: HKQueryAnchor.self, from: data)
  80. }
  81. }
  82. @Persisted(key: "HealthKitManagerAnchor") private var persistedBGAnchor: Data? = nil
  83. var isAvailableOnCurrentDevice: Bool {
  84. HKHealthStore.isHealthDataAvailable()
  85. }
  86. var areAllowAllPermissions: Bool {
  87. Set(Config.readPermissions.map { healthKitStore.authorizationStatus(for: $0) })
  88. .intersection([.notDetermined])
  89. .isEmpty &&
  90. Set(Config.writePermissions.map { healthKitStore.authorizationStatus(for: $0) })
  91. .intersection([.sharingDenied, .notDetermined])
  92. .isEmpty
  93. }
  94. // NSPredicate, which use during load increment BG from Health store
  95. private var loadBGPredicate: NSPredicate {
  96. // loading only daily bg
  97. let predicateByStartDate = HKQuery.predicateForSamples(
  98. withStart: Date().addingTimeInterval(-1.days.timeInterval),
  99. end: nil,
  100. options: .strictStartDate
  101. )
  102. // loading only not FreeAPS bg
  103. // this predicate dont influence on Deleted Objects, only on added
  104. let predicateByMeta = HKQuery.predicateForObjects(
  105. withMetadataKey: Config.freeAPSMetaKey,
  106. operatorType: .notEqualTo,
  107. value: 1
  108. )
  109. return NSCompoundPredicate(andPredicateWithSubpredicates: [predicateByStartDate, predicateByMeta])
  110. }
  111. init(resolver: Resolver) {
  112. injectServices(resolver)
  113. guard isAvailableOnCurrentDevice,
  114. Config.healthBGObject != nil else { return }
  115. carbsStorage.delegate = self
  116. pumpHistoryStorage.delegate = self
  117. debug(.service, "HealthKitManager did create")
  118. }
  119. func checkAvailabilitySave(objectTypeToHealthStore: HKObjectType) -> Bool {
  120. healthKitStore.authorizationStatus(for: objectTypeToHealthStore) == .sharingAuthorized
  121. }
  122. func checkAvailabilitySaveBG() -> Bool {
  123. Config.healthBGObject.map { checkAvailabilitySave(objectTypeToHealthStore: $0) } ?? false
  124. }
  125. func requestPermission() async throws -> Bool {
  126. guard isAvailableOnCurrentDevice else {
  127. throw HKError.notAvailableOnCurrentDevice
  128. }
  129. guard Config.readPermissions.isNotEmpty, Config.writePermissions.isNotEmpty else {
  130. throw HKError.dataNotAvailable
  131. }
  132. return try await withCheckedThrowingContinuation { continuation in
  133. healthKitStore.requestAuthorization(toShare: Config.writePermissions, read: Config.readPermissions) { status, error in
  134. if let error = error {
  135. continuation.resume(throwing: error)
  136. } else {
  137. continuation.resume(returning: status)
  138. }
  139. }
  140. }
  141. }
  142. // Glucose Upload
  143. func uploadGlucose() async {
  144. await uploadGlucose(glucoseStorage.getGlucoseNotYetUploadedToHealth())
  145. await uploadGlucose(glucoseStorage.getManualGlucoseNotYetUploadedToHealth())
  146. }
  147. func uploadGlucose(_ glucose: [BloodGlucose]) async {
  148. guard settingsManager.settings.useAppleHealth,
  149. let sampleType = Config.healthBGObject,
  150. checkAvailabilitySave(objectTypeToHealthStore: sampleType),
  151. glucose.isNotEmpty
  152. else { return }
  153. do {
  154. // Create HealthKit samples from all the passed glucose values
  155. let glucoseSamples = glucose.compactMap { glucoseSample -> HKQuantitySample? in
  156. guard let glucoseValue = glucoseSample.glucose else { return nil }
  157. return HKQuantitySample(
  158. type: sampleType,
  159. quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: Double(glucoseValue)),
  160. start: glucoseSample.dateString,
  161. end: glucoseSample.dateString,
  162. metadata: [
  163. HKMetadataKeyExternalUUID: glucoseSample.id,
  164. HKMetadataKeySyncIdentifier: glucoseSample.id,
  165. HKMetadataKeySyncVersion: 1,
  166. Config.freeAPSMetaKey: true
  167. ]
  168. )
  169. }
  170. guard glucoseSamples.isNotEmpty else {
  171. debug(.service, "No glucose samples available for upload.")
  172. return
  173. }
  174. // Attempt to save the blood glucose samples to Apple Health
  175. try await healthKitStore.save(glucoseSamples)
  176. debug(.service, "Successfully stored \(glucoseSamples.count) blood glucose samples in HealthKit.")
  177. // After successful upload, update the isUploadedToHealth flag in Core Data
  178. await updateGlucoseAsUploaded(glucose)
  179. } catch {
  180. debug(.service, "Failed to upload glucose samples to HealthKit: \(error.localizedDescription)")
  181. }
  182. }
  183. private func updateGlucoseAsUploaded(_ glucose: [BloodGlucose]) async {
  184. await backgroundContext.perform {
  185. let ids = glucose.map(\.id) as NSArray
  186. let fetchRequest: NSFetchRequest<GlucoseStored> = GlucoseStored.fetchRequest()
  187. fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
  188. do {
  189. let results = try self.backgroundContext.fetch(fetchRequest)
  190. for result in results {
  191. result.isUploadedToHealth = true
  192. }
  193. guard self.backgroundContext.hasChanges else { return }
  194. try self.backgroundContext.save()
  195. } catch let error as NSError {
  196. debugPrint(
  197. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToHealth: \(error.userInfo)"
  198. )
  199. }
  200. }
  201. }
  202. // Carbs Upload
  203. func uploadCarbs() async {
  204. await uploadCarbs(carbsStorage.getCarbsNotYetUploadedToHealth())
  205. }
  206. func uploadCarbs(_ carbs: [CarbsEntry]) async {
  207. guard settingsManager.settings.useAppleHealth,
  208. let carbSampleType = Config.healthCarbObject,
  209. let fatSampleType = Config.healthFatObject,
  210. let proteinSampleType = Config.healthProteinObject,
  211. checkAvailabilitySave(objectTypeToHealthStore: carbSampleType),
  212. carbs.isNotEmpty
  213. else { return }
  214. do {
  215. var samples: [HKQuantitySample] = []
  216. // Create HealthKit samples for carbs, fat, and protein
  217. for allSamples in carbs {
  218. guard let id = allSamples.id else { continue }
  219. let startDate = allSamples.actualDate ?? Date()
  220. // Carbs Sample
  221. let carbValue = allSamples.carbs
  222. let carbSample = HKQuantitySample(
  223. type: carbSampleType,
  224. quantity: HKQuantity(unit: .gram(), doubleValue: Double(carbValue)),
  225. start: startDate,
  226. end: startDate,
  227. metadata: [
  228. HKMetadataKeyExternalUUID: id,
  229. HKMetadataKeySyncIdentifier: id,
  230. HKMetadataKeySyncVersion: 1,
  231. Config.freeAPSMetaKey: true
  232. ]
  233. )
  234. samples.append(carbSample)
  235. // Fat Sample (if available)
  236. if let fatValue = allSamples.fat {
  237. let fatSample = HKQuantitySample(
  238. type: fatSampleType,
  239. quantity: HKQuantity(unit: .gram(), doubleValue: Double(fatValue)),
  240. start: startDate,
  241. end: startDate,
  242. metadata: [
  243. HKMetadataKeyExternalUUID: id,
  244. HKMetadataKeySyncIdentifier: id,
  245. HKMetadataKeySyncVersion: 1,
  246. Config.freeAPSMetaKey: true
  247. ]
  248. )
  249. samples.append(fatSample)
  250. }
  251. // Protein Sample (if available)
  252. if let proteinValue = allSamples.protein {
  253. let proteinSample = HKQuantitySample(
  254. type: proteinSampleType,
  255. quantity: HKQuantity(unit: .gram(), doubleValue: Double(proteinValue)),
  256. start: startDate,
  257. end: startDate,
  258. metadata: [
  259. HKMetadataKeyExternalUUID: id,
  260. HKMetadataKeySyncIdentifier: id,
  261. HKMetadataKeySyncVersion: 1,
  262. Config.freeAPSMetaKey: true
  263. ]
  264. )
  265. samples.append(proteinSample)
  266. }
  267. }
  268. // Attempt to save the samples to Apple Health
  269. guard samples.isNotEmpty else {
  270. debug(.service, "No samples available for upload.")
  271. return
  272. }
  273. try await healthKitStore.save(samples)
  274. debug(.service, "Successfully stored \(samples.count) carb samples in HealthKit.")
  275. // After successful upload, update the isUploadedToHealth flag in Core Data
  276. await updateCarbsAsUploaded(carbs)
  277. } catch {
  278. debug(.service, "Failed to upload carb samples to HealthKit: \(error.localizedDescription)")
  279. }
  280. }
  281. private func updateCarbsAsUploaded(_ carbs: [CarbsEntry]) async {
  282. await backgroundContext.perform {
  283. let ids = carbs.map(\.id) as NSArray
  284. let fetchRequest: NSFetchRequest<CarbEntryStored> = CarbEntryStored.fetchRequest()
  285. fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
  286. do {
  287. let results = try self.backgroundContext.fetch(fetchRequest)
  288. for result in results {
  289. result.isUploadedToHealth = true
  290. }
  291. guard self.backgroundContext.hasChanges else { return }
  292. try self.backgroundContext.save()
  293. } catch let error as NSError {
  294. debugPrint(
  295. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToHealth: \(error.userInfo)"
  296. )
  297. }
  298. }
  299. }
  300. // Insulin Upload
  301. func uploadInsulin() async {
  302. await uploadInsulin(pumpHistoryStorage.getPumpHistoryNotYetUploadedToHealth())
  303. }
  304. func uploadInsulin(_ insulin: [PumpHistoryEvent]) async {
  305. guard settingsManager.settings.useAppleHealth,
  306. let sampleType = Config.healthInsulinObject,
  307. checkAvailabilitySave(objectTypeToHealthStore: sampleType),
  308. insulin.isNotEmpty
  309. else { return }
  310. do {
  311. let insulinSamples = insulin.compactMap { insulinSample -> HKQuantitySample? in
  312. guard let insulinValue = insulinSample.amount else { return nil }
  313. // Determine the insulin delivery reason (bolus or basal)
  314. let deliveryReason: HKInsulinDeliveryReason
  315. switch insulinSample.type {
  316. case .bolus:
  317. deliveryReason = .bolus
  318. case .tempBasal:
  319. deliveryReason = .basal
  320. default:
  321. // Skip other types
  322. /// If deliveryReason is nil, the compactMap will filter this sample out preventing a crash
  323. return nil
  324. }
  325. return HKQuantitySample(
  326. type: sampleType,
  327. quantity: HKQuantity(unit: .internationalUnit(), doubleValue: Double(insulinValue)),
  328. start: insulinSample.timestamp,
  329. end: insulinSample.timestamp,
  330. metadata: [
  331. HKMetadataKeyExternalUUID: insulinSample.id,
  332. HKMetadataKeySyncIdentifier: insulinSample.id,
  333. HKMetadataKeySyncVersion: 1,
  334. HKMetadataKeyInsulinDeliveryReason: deliveryReason.rawValue,
  335. Config.freeAPSMetaKey: true
  336. ]
  337. )
  338. }
  339. guard insulinSamples.isNotEmpty else {
  340. debug(.service, "No insulin samples available for upload.")
  341. return
  342. }
  343. // Attempt to save the insulin samples to Apple Health
  344. try await healthKitStore.save(insulinSamples)
  345. debug(.service, "Successfully stored \(insulinSamples.count) insulin samples in HealthKit.")
  346. // After successful upload, update the isUploadedToHealth flag in Core Data
  347. await updateInsulinAsUploaded(insulin)
  348. } catch {
  349. debug(.service, "Failed to upload insulin samples to HealthKit: \(error.localizedDescription)")
  350. }
  351. }
  352. private func updateInsulinAsUploaded(_ insulin: [PumpHistoryEvent]) async {
  353. await backgroundContext.perform {
  354. let ids = insulin.map(\.id) as NSArray
  355. let fetchRequest: NSFetchRequest<PumpEventStored> = PumpEventStored.fetchRequest()
  356. fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
  357. do {
  358. let results = try self.backgroundContext.fetch(fetchRequest)
  359. for result in results {
  360. result.isUploadedToHealth = true
  361. }
  362. guard self.backgroundContext.hasChanges else { return }
  363. try self.backgroundContext.save()
  364. } catch let error as NSError {
  365. debugPrint(
  366. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToHealth: \(error.userInfo)"
  367. )
  368. }
  369. }
  370. }
  371. // Delete Glucose/Carbs/Insulin
  372. func deleteGlucose(syncID: String) async {
  373. guard settingsManager.settings.useAppleHealth,
  374. let sampleType = Config.healthBGObject,
  375. checkAvailabilitySave(objectTypeToHealthStore: sampleType)
  376. else { return }
  377. let predicate = HKQuery.predicateForObjects(
  378. withMetadataKey: HKMetadataKeySyncIdentifier,
  379. operatorType: .equalTo,
  380. value: syncID
  381. )
  382. do {
  383. try await deleteObjects(of: sampleType, predicate: predicate)
  384. debug(.service, "Successfully deleted glucose sample with syncID: \(syncID)")
  385. } catch {
  386. warning(.service, "Failed to delete glucose sample with syncID: \(syncID)", error: error)
  387. }
  388. }
  389. private func deleteObjects(of sampleType: HKSampleType, predicate: NSPredicate) async throws {
  390. try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
  391. healthKitStore.deleteObjects(of: sampleType, predicate: predicate) { success, _, error in
  392. if let error = error {
  393. continuation.resume(throwing: error)
  394. } else if success {
  395. continuation.resume(returning: ())
  396. }
  397. }
  398. }
  399. }
  400. // Observer that notifies when new Glucose values arrive in Apple Health
  401. func createBGObserver() {
  402. guard settingsManager.settings.useAppleHealth else { return }
  403. guard let bgType = Config.healthBGObject else {
  404. warning(.service, "Can not create HealthKit Observer, because unable to get the Blood Glucose type")
  405. return
  406. }
  407. let query = HKObserverQuery(sampleType: bgType, predicate: nil) { [weak self] _, _, observerError in
  408. guard let self = self else { return }
  409. debug(.service, "Execute HealthKit observer query for loading increment samples")
  410. guard observerError == nil else {
  411. warning(.service, "Error during execution of HealthKit Observer's query", error: observerError!)
  412. return
  413. }
  414. if let incrementQuery = self.getBloodGlucoseHKQuery(predicate: self.loadBGPredicate) {
  415. debug(.service, "Create increment query")
  416. self.healthKitStore.execute(incrementQuery)
  417. }
  418. }
  419. healthKitStore.execute(query)
  420. debug(.service, "Create Observer for Blood Glucose")
  421. }
  422. func enableBackgroundDelivery() {
  423. guard settingsManager.settings.useAppleHealth else {
  424. healthKitStore.disableAllBackgroundDelivery { _, _ in }
  425. return }
  426. guard let bgType = Config.healthBGObject else {
  427. warning(
  428. .service,
  429. "Can not create background delivery, because unable to get the Blood Glucose type"
  430. )
  431. return
  432. }
  433. healthKitStore.enableBackgroundDelivery(for: bgType, frequency: .immediate) { status, error in
  434. guard error == nil else {
  435. warning(.service, "Can not enable background delivery", error: error)
  436. return
  437. }
  438. debug(.service, "Background delivery status is \(status)")
  439. }
  440. }
  441. private func getBloodGlucoseHKQuery(predicate: NSPredicate) -> HKQuery? {
  442. guard let sampleType = Config.healthBGObject else { return nil }
  443. let query = HKAnchoredObjectQuery(
  444. type: sampleType,
  445. predicate: predicate,
  446. anchor: lastBloodGlucoseQueryAnchor,
  447. limit: HKObjectQueryNoLimit
  448. ) { [weak self] _, addedObjects, _, anchor, _ in
  449. guard let self = self else { return }
  450. self.processQueue.async {
  451. debug(.service, "AnchoredQuery did execute")
  452. self.lastBloodGlucoseQueryAnchor = anchor
  453. // Added objects
  454. if let bgSamples = addedObjects as? [HKQuantitySample],
  455. bgSamples.isNotEmpty
  456. {
  457. self.prepareBGSamplesToPublisherFetch(bgSamples)
  458. }
  459. }
  460. }
  461. return query
  462. }
  463. private func prepareBGSamplesToPublisherFetch(_ samples: [HKQuantitySample]) {
  464. dispatchPrecondition(condition: .onQueue(processQueue))
  465. newGlucose += samples
  466. .compactMap { sample -> HealthKitSample? in
  467. let fromTrio = sample.metadata?[Config.freeAPSMetaKey] as? Bool ?? false
  468. guard !fromTrio else { return nil }
  469. return HealthKitSample(
  470. healthKitId: sample.uuid.uuidString,
  471. date: sample.startDate,
  472. glucose: Int(round(sample.quantity.doubleValue(for: .milligramsPerDeciliter)))
  473. )
  474. }
  475. .map { sample in
  476. BloodGlucose(
  477. _id: sample.healthKitId,
  478. sgv: sample.glucose,
  479. direction: nil,
  480. date: Decimal(Int(sample.date.timeIntervalSince1970) * 1000),
  481. dateString: sample.date,
  482. unfiltered: Decimal(sample.glucose),
  483. filtered: nil,
  484. noise: nil,
  485. glucose: sample.glucose,
  486. type: "sgv"
  487. )
  488. }
  489. .filter { $0.dateString >= Date().addingTimeInterval(-1.days.timeInterval) }
  490. newGlucose = newGlucose.removeDublicates()
  491. }
  492. // MARK: - GlucoseSource
  493. var glucoseManager: FetchGlucoseManager?
  494. var cgmManager: CGMManagerUI?
  495. func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
  496. Future { [weak self] promise in
  497. guard let self = self else {
  498. promise(.success([]))
  499. return
  500. }
  501. self.processQueue.async {
  502. guard self.settingsManager.settings.useAppleHealth else {
  503. promise(.success([]))
  504. return
  505. }
  506. // Remove old BGs
  507. self.newGlucose = self.newGlucose
  508. .filter { $0.dateString >= Date().addingTimeInterval(-1.days.timeInterval) }
  509. // Get actual BGs (beetwen Date() - 1 day and Date())
  510. let actualGlucose = self.newGlucose
  511. .filter { $0.dateString <= Date() }
  512. // Update newGlucose
  513. self.newGlucose = self.newGlucose
  514. .filter { !actualGlucose.contains($0) }
  515. // debug(.service, "Actual glucose is \(actualGlucose)")
  516. // debug(.service, "Current state of newGlucose is \(self.newGlucose)")
  517. promise(.success(actualGlucose))
  518. }
  519. }
  520. .eraseToAnyPublisher()
  521. }
  522. func fetchIfNeeded() -> AnyPublisher<[BloodGlucose], Never> {
  523. fetch(nil)
  524. }
  525. // - MARK Carbs function
  526. func deleteCarbs(syncID: String, fpuID: String) {
  527. guard settingsManager.settings.useAppleHealth,
  528. let sampleType = Config.healthCarbObject,
  529. checkAvailabilitySave(objectTypeToHealthStore: sampleType)
  530. else { return }
  531. print("meals 4: ID: " + syncID + " FPU ID: " + fpuID)
  532. if syncID != "" {
  533. let predicate = HKQuery.predicateForObjects(
  534. withMetadataKey: HKMetadataKeySyncIdentifier,
  535. operatorType: .equalTo,
  536. value: syncID
  537. )
  538. healthKitStore.deleteObjects(of: sampleType, predicate: predicate) { _, _, error in
  539. guard let error = error else { return }
  540. warning(.service, "Cannot delete sample with syncID: \(syncID)", error: error)
  541. }
  542. }
  543. if fpuID != "" {
  544. // processQueue.async {
  545. let recentCarbs: [CarbsEntry] = carbsStorage.recent()
  546. let ids = recentCarbs.filter { $0.fpuID == fpuID }.compactMap(\.id)
  547. let predicate = HKQuery.predicateForObjects(
  548. withMetadataKey: HKMetadataKeySyncIdentifier,
  549. allowedValues: ids
  550. )
  551. print("found IDs: " + ids.description)
  552. healthKitStore.deleteObjects(of: sampleType, predicate: predicate) { _, _, error in
  553. guard let error = error else { return }
  554. warning(.service, "Cannot delete sample with fpuID: \(fpuID)", error: error)
  555. }
  556. // }
  557. }
  558. }
  559. // - MARK Insulin function
  560. func deleteInsulin(syncID: String) {
  561. guard settingsManager.settings.useAppleHealth,
  562. let sampleType = Config.healthInsulinObject,
  563. checkAvailabilitySave(objectTypeToHealthStore: sampleType)
  564. else { return }
  565. processQueue.async {
  566. let predicate = HKQuery.predicateForObjects(
  567. withMetadataKey: HKMetadataKeySyncIdentifier,
  568. operatorType: .equalTo,
  569. value: syncID
  570. )
  571. self.healthKitStore.deleteObjects(of: sampleType, predicate: predicate) { _, _, error in
  572. guard let error = error else { return }
  573. warning(.service, "Cannot delete sample with syncID: \(syncID)", error: error)
  574. }
  575. }
  576. }
  577. }
  578. enum HealthKitPermissionRequestStatus {
  579. case needRequest
  580. case didRequest
  581. }
  582. enum HKError: Error {
  583. // HealthKit work only iPhone (not on iPad)
  584. case notAvailableOnCurrentDevice
  585. // Some data can be not available on current iOS-device
  586. case dataNotAvailable
  587. }
  588. private struct InsulinBolus {
  589. var id: String
  590. var amount: Decimal
  591. var date: Date
  592. }
  593. private struct InsulinBasal {
  594. var id: String
  595. var amount: Decimal
  596. var startDelivery: Date
  597. var endDelivery: Date
  598. }