HealthKitManager.swift 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772
  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 saveIfNeeded(pumpEvents events: [PumpHistoryEvent])
  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)
  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, CarbsObserver, PumpHistoryObserver {
  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, 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 healthInsulinObject = HKObjectType.quantityType(forIdentifier: .insulinDelivery)
  44. // Meta-data key of FreeASPX data in HealthStore
  45. static let freeAPSMetaKey = "From Trio"
  46. }
  47. @Injected() private var glucoseStorage: GlucoseStorage!
  48. @Injected() private var healthKitStore: HKHealthStore!
  49. @Injected() private var settingsManager: SettingsManager!
  50. @Injected() private var broadcaster: Broadcaster!
  51. @Injected() var carbsStorage: CarbsStorage!
  52. private var backgroundContext = CoreDataStack.shared.newTaskContext()
  53. private let processQueue = DispatchQueue(label: "BaseHealthKitManager.processQueue")
  54. private var lifetime = Lifetime()
  55. // BG that will be return Publisher
  56. @SyncAccess @Persisted(key: "BaseHealthKitManager.newGlucose") private var newGlucose: [BloodGlucose] = []
  57. // last anchor for HKAnchoredQuery
  58. private var lastBloodGlucoseQueryAnchor: HKQueryAnchor? {
  59. set {
  60. persistedBGAnchor = try? NSKeyedArchiver.archivedData(withRootObject: newValue as Any, requiringSecureCoding: false)
  61. }
  62. get {
  63. guard let data = persistedBGAnchor else { return nil }
  64. return try? NSKeyedUnarchiver.unarchivedObject(ofClass: HKQueryAnchor.self, from: data)
  65. }
  66. }
  67. @Persisted(key: "HealthKitManagerAnchor") private var persistedBGAnchor: Data? = nil
  68. var isAvailableOnCurrentDevice: Bool {
  69. HKHealthStore.isHealthDataAvailable()
  70. }
  71. var areAllowAllPermissions: Bool {
  72. Set(Config.readPermissions.map { healthKitStore.authorizationStatus(for: $0) })
  73. .intersection([.notDetermined])
  74. .isEmpty &&
  75. Set(Config.writePermissions.map { healthKitStore.authorizationStatus(for: $0) })
  76. .intersection([.sharingDenied, .notDetermined])
  77. .isEmpty
  78. }
  79. // NSPredicate, which use during load increment BG from Health store
  80. private var loadBGPredicate: NSPredicate {
  81. // loading only daily bg
  82. let predicateByStartDate = HKQuery.predicateForSamples(
  83. withStart: Date().addingTimeInterval(-1.days.timeInterval),
  84. end: nil,
  85. options: .strictStartDate
  86. )
  87. // loading only not FreeAPS bg
  88. // this predicate dont influence on Deleted Objects, only on added
  89. let predicateByMeta = HKQuery.predicateForObjects(
  90. withMetadataKey: Config.freeAPSMetaKey,
  91. operatorType: .notEqualTo,
  92. value: 1
  93. )
  94. return NSCompoundPredicate(andPredicateWithSubpredicates: [predicateByStartDate, predicateByMeta])
  95. }
  96. init(resolver: Resolver) {
  97. injectServices(resolver)
  98. guard isAvailableOnCurrentDevice,
  99. Config.healthBGObject != nil else { return }
  100. broadcaster.register(CarbsObserver.self, observer: self)
  101. broadcaster.register(PumpHistoryObserver.self, observer: self)
  102. debug(.service, "HealthKitManager did create")
  103. }
  104. func checkAvailabilitySave(objectTypeToHealthStore: HKObjectType) -> Bool {
  105. healthKitStore.authorizationStatus(for: objectTypeToHealthStore) == .sharingAuthorized
  106. }
  107. func checkAvailabilitySaveBG() -> Bool {
  108. Config.healthBGObject.map { checkAvailabilitySave(objectTypeToHealthStore: $0) } ?? false
  109. }
  110. func requestPermission() async throws -> Bool {
  111. guard isAvailableOnCurrentDevice else {
  112. throw HKError.notAvailableOnCurrentDevice
  113. }
  114. guard Config.readPermissions.isNotEmpty, Config.writePermissions.isNotEmpty else {
  115. throw HKError.dataNotAvailable
  116. }
  117. return try await withCheckedThrowingContinuation { continuation in
  118. healthKitStore.requestAuthorization(toShare: Config.writePermissions, read: Config.readPermissions) { status, error in
  119. if let error = error {
  120. continuation.resume(throwing: error)
  121. } else {
  122. continuation.resume(returning: status)
  123. }
  124. }
  125. }
  126. }
  127. // Glucose Upload
  128. func uploadGlucose() async {
  129. await uploadGlucose(glucoseStorage.getGlucoseNotYetUploadedToHealth())
  130. await uploadGlucose(glucoseStorage.getManualGlucoseNotYetUploadedToHealth())
  131. }
  132. func uploadGlucose(_ glucose: [BloodGlucose]) async {
  133. guard settingsManager.settings.useAppleHealth,
  134. let sampleType = Config.healthBGObject,
  135. checkAvailabilitySave(objectTypeToHealthStore: sampleType),
  136. glucose.isNotEmpty
  137. else { return }
  138. do {
  139. // Create HealthKit samples from all the passed glucose values
  140. let glucoseSamples = glucose.compactMap { glucoseSample -> HKQuantitySample? in
  141. guard let glucoseValue = glucoseSample.glucose else { return nil }
  142. return HKQuantitySample(
  143. type: sampleType,
  144. quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: Double(glucoseValue)),
  145. start: glucoseSample.dateString,
  146. end: glucoseSample.dateString,
  147. metadata: [
  148. HKMetadataKeyExternalUUID: glucoseSample.id,
  149. HKMetadataKeySyncIdentifier: glucoseSample.id,
  150. HKMetadataKeySyncVersion: 1,
  151. Config.freeAPSMetaKey: true
  152. ]
  153. )
  154. }
  155. guard glucoseSamples.isNotEmpty else {
  156. debug(.service, "No glucose samples available for upload.")
  157. return
  158. }
  159. // Attempt to save the blood glucose samples to Apple Health
  160. try await healthKitStore.save(glucoseSamples)
  161. debug(.service, "Successfully stored \(glucoseSamples.count) blood glucose samples in HealthKit.")
  162. // After successful upload, update the isUploadedToHealth flag in Core Data
  163. await updateGlucoseAsUploaded(glucose)
  164. } catch {
  165. debug(.service, "Failed to upload glucose samples to HealthKit: \(error.localizedDescription)")
  166. }
  167. }
  168. private func updateGlucoseAsUploaded(_ glucose: [BloodGlucose]) async {
  169. await backgroundContext.perform {
  170. let ids = glucose.map(\.id) as NSArray
  171. let fetchRequest: NSFetchRequest<GlucoseStored> = GlucoseStored.fetchRequest()
  172. fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
  173. do {
  174. let results = try self.backgroundContext.fetch(fetchRequest)
  175. for result in results {
  176. result.isUploadedToHealth = true
  177. }
  178. guard self.backgroundContext.hasChanges else { return }
  179. try self.backgroundContext.save()
  180. } catch let error as NSError {
  181. debugPrint(
  182. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToHealth: \(error.userInfo)"
  183. )
  184. }
  185. }
  186. }
  187. // Carbs Upload
  188. func uploadCarbs() async {
  189. await uploadGlucose(glucoseStorage.getGlucoseNotYetUploadedToHealth())
  190. await uploadGlucose(glucoseStorage.getManualGlucoseNotYetUploadedToHealth())
  191. }
  192. func uploadCarbs(_ carbs: [CarbsEntry]) async {
  193. guard settingsManager.settings.useAppleHealth,
  194. let sampleType = Config.healthCarbObject,
  195. checkAvailabilitySave(objectTypeToHealthStore: sampleType),
  196. carbs.isNotEmpty
  197. else { return }
  198. do {
  199. // Create HealthKit samples from all the passed carb values
  200. let carbSamples = carbs.compactMap { carbSample -> HKQuantitySample? in
  201. guard let id = carbSample.id else { return nil }
  202. let carbValue = carbSample.carbs
  203. return HKQuantitySample(
  204. type: sampleType,
  205. quantity: HKQuantity(unit: .gram(), doubleValue: Double(carbValue)),
  206. start: carbSample.actualDate ?? Date(),
  207. end: carbSample.actualDate ?? Date(),
  208. metadata: [
  209. HKMetadataKeyExternalUUID: id,
  210. HKMetadataKeySyncIdentifier: id,
  211. HKMetadataKeySyncVersion: 1,
  212. Config.freeAPSMetaKey: true
  213. ]
  214. )
  215. }
  216. guard carbSamples.isNotEmpty else {
  217. debug(.service, "No glucose samples available for upload.")
  218. return
  219. }
  220. // Attempt to save the blood glucose samples to Apple Health
  221. try await healthKitStore.save(carbSamples)
  222. debug(.service, "Successfully stored \(carbSamples.count) carb samples in HealthKit.")
  223. // After successful upload, update the isUploadedToHealth flag in Core Data
  224. await updateCarbsAsUploaded(carbs)
  225. } catch {
  226. debug(.service, "Failed to upload carb samples to HealthKit: \(error.localizedDescription)")
  227. }
  228. }
  229. private func updateCarbsAsUploaded(_ carbs: [CarbsEntry]) async {
  230. await backgroundContext.perform {
  231. let ids = carbs.map(\.id) as NSArray
  232. let fetchRequest: NSFetchRequest<CarbEntryStored> = CarbEntryStored.fetchRequest()
  233. fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
  234. do {
  235. let results = try self.backgroundContext.fetch(fetchRequest)
  236. for result in results {
  237. result.isUploadedToHealth = true
  238. }
  239. guard self.backgroundContext.hasChanges else { return }
  240. try self.backgroundContext.save()
  241. } catch let error as NSError {
  242. debugPrint(
  243. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToHealth: \(error.userInfo)"
  244. )
  245. }
  246. }
  247. }
  248. func saveIfNeeded(carbs: [CarbsEntry]) {
  249. guard settingsManager.settings.useAppleHealth,
  250. let sampleType = Config.healthCarbObject,
  251. checkAvailabilitySave(objectTypeToHealthStore: sampleType),
  252. carbs.isNotEmpty
  253. else { return }
  254. let carbsWithId = carbs.filter { c in
  255. guard c.id != nil else { return false }
  256. return true
  257. }
  258. func save(samples: [HKSample]) {
  259. let sampleIDs = samples.compactMap(\.syncIdentifier)
  260. let sampleDates = samples.map(\.startDate)
  261. let samplesToSave = carbsWithId
  262. .filter { !sampleIDs.contains($0.id ?? "") } // id existing in AH
  263. // .filter { !sampleDates.contains($0.actualDate ?? $0.createdAt) } // not id but exactly the same datetime
  264. .filter { !sampleDates.contains($0.createdAt) } // not id but exactly the same datetime
  265. .map {
  266. HKQuantitySample(
  267. type: sampleType,
  268. quantity: HKQuantity(unit: .gram(), doubleValue: Double($0.carbs)),
  269. start: $0.actualDate ?? $0.createdAt,
  270. end: $0.actualDate ?? $0.createdAt,
  271. metadata: [
  272. HKMetadataKeySyncIdentifier: $0.id ?? "_id",
  273. HKMetadataKeySyncVersion: 1,
  274. Config.freeAPSMetaKey: true
  275. ]
  276. )
  277. }
  278. healthKitStore.save(samplesToSave) { (success: Bool, error: Error?) -> Void in
  279. if !success {
  280. debug(.service, "Failed to store carb entry in HealthKit Store!")
  281. debug(.service, error?.localizedDescription ?? "Unknown error")
  282. }
  283. }
  284. }
  285. loadSamplesFromHealth(sampleType: sampleType, completion: { samples in
  286. save(samples: samples)
  287. })
  288. }
  289. func saveIfNeeded(pumpEvents events: [PumpHistoryEvent]) {
  290. guard settingsManager.settings.useAppleHealth,
  291. let sampleType = Config.healthInsulinObject,
  292. checkAvailabilitySave(objectTypeToHealthStore: sampleType),
  293. events.isNotEmpty
  294. else { return }
  295. func save(bolusToModify: [InsulinBolus], bolus: [InsulinBolus], basal: [InsulinBasal]) {
  296. // first step : delete the HK value
  297. // second step : recreate with the new value !
  298. bolusToModify.forEach { syncID in
  299. let predicate = HKQuery.predicateForObjects(
  300. withMetadataKey: HKMetadataKeySyncIdentifier,
  301. operatorType: .equalTo,
  302. value: syncID.id
  303. )
  304. self.healthKitStore.deleteObjects(of: sampleType, predicate: predicate) { _, _, error in
  305. guard let error = error else { return }
  306. warning(.service, "Cannot delete sample with syncID: \(syncID.id)", error: error)
  307. }
  308. }
  309. let bolusTotal = bolus + bolusToModify
  310. let bolusSamples = bolusTotal
  311. .map {
  312. HKQuantitySample(
  313. type: sampleType,
  314. quantity: HKQuantity(unit: .internationalUnit(), doubleValue: Double($0.amount)),
  315. start: $0.date,
  316. end: $0.date,
  317. metadata: [
  318. HKMetadataKeyInsulinDeliveryReason: NSNumber(2),
  319. HKMetadataKeyExternalUUID: $0.id,
  320. HKMetadataKeySyncIdentifier: $0.id,
  321. HKMetadataKeySyncVersion: 1,
  322. Config.freeAPSMetaKey: true
  323. ]
  324. )
  325. }
  326. let basalSamples = basal
  327. .map {
  328. HKQuantitySample(
  329. type: sampleType,
  330. quantity: HKQuantity(unit: .internationalUnit(), doubleValue: Double($0.amount)),
  331. start: $0.startDelivery,
  332. end: $0.endDelivery,
  333. metadata: [
  334. HKMetadataKeyInsulinDeliveryReason: NSNumber(1),
  335. HKMetadataKeyExternalUUID: $0.id,
  336. HKMetadataKeySyncIdentifier: $0.id,
  337. HKMetadataKeySyncVersion: 1,
  338. Config.freeAPSMetaKey: true
  339. ]
  340. )
  341. }
  342. healthKitStore.save(bolusSamples + basalSamples) { (success: Bool, error: Error?) -> Void in
  343. if !success {
  344. debug(.service, "Failed to store insulin entry in HealthKit Store!")
  345. debug(.service, error?.localizedDescription ?? "Unknown error")
  346. }
  347. }
  348. }
  349. loadSamplesFromHealth(sampleType: sampleType, withIDs: events.map(\.id), completion: { samples in
  350. let sampleIDs = samples.compactMap(\.syncIdentifier)
  351. let bolusToModify = events
  352. .filter { $0.type == .bolus && sampleIDs.contains($0.id) }
  353. .compactMap { event -> InsulinBolus? in
  354. guard let amount = event.amount else { return nil }
  355. guard let sampleAmount = samples.first(where: { $0.syncIdentifier == event.id }) as? HKQuantitySample
  356. else { return nil }
  357. if Double(amount) != sampleAmount.quantity.doubleValue(for: .internationalUnit()) {
  358. return InsulinBolus(id: sampleAmount.syncIdentifier!, amount: amount, date: event.timestamp)
  359. } else { return nil }
  360. }
  361. let bolus = events
  362. .filter { $0.type == .bolus && !sampleIDs.contains($0.id) }
  363. .compactMap { event -> InsulinBolus? in
  364. guard let amount = event.amount else { return nil }
  365. return InsulinBolus(id: event.id, amount: amount, date: event.timestamp)
  366. }
  367. let basalEvents = events
  368. .filter { $0.type == .tempBasal && !sampleIDs.contains($0.id) }
  369. .sorted(by: { $0.timestamp < $1.timestamp })
  370. let basal = basalEvents.enumerated()
  371. .compactMap { item -> InsulinBasal? in
  372. let nextElementEventIndex = item.offset + 1
  373. guard basalEvents.count > nextElementEventIndex else { return nil }
  374. var minimalDose = self.settingsManager.preferences.bolusIncrement
  375. if (minimalDose != 0.05) || (minimalDose != 0.025) {
  376. minimalDose = Decimal(0.05)
  377. }
  378. let nextBasalEvent = basalEvents[nextElementEventIndex]
  379. let secondsOfCurrentBasal = nextBasalEvent.timestamp.timeIntervalSince(item.element.timestamp)
  380. let amount = Decimal(secondsOfCurrentBasal / 3600) * (item.element.rate ?? 0)
  381. let incrementsRaw = amount / minimalDose
  382. var amountRounded: Decimal
  383. if incrementsRaw >= 1 {
  384. let incrementsRounded = floor(Double(incrementsRaw))
  385. amountRounded = Decimal(round(incrementsRounded * Double(minimalDose) * 100_000.0) / 100_000.0)
  386. } else {
  387. amountRounded = 0
  388. }
  389. let id = String(item.element.id.dropFirst())
  390. guard amountRounded > 0,
  391. id != ""
  392. else { return nil }
  393. return InsulinBasal(
  394. id: id,
  395. amount: amountRounded,
  396. startDelivery: item.element.timestamp,
  397. endDelivery: nextBasalEvent.timestamp
  398. )
  399. }
  400. save(bolusToModify: bolusToModify, bolus: bolus, basal: basal)
  401. })
  402. }
  403. func pumpHistoryDidUpdate(_ events: [PumpHistoryEvent]) {
  404. saveIfNeeded(pumpEvents: events)
  405. }
  406. func createBGObserver() {
  407. guard settingsManager.settings.useAppleHealth else { return }
  408. guard let bgType = Config.healthBGObject else {
  409. warning(.service, "Can not create HealthKit Observer, because unable to get the Blood Glucose type")
  410. return
  411. }
  412. let query = HKObserverQuery(sampleType: bgType, predicate: nil) { [weak self] _, _, observerError in
  413. guard let self = self else { return }
  414. debug(.service, "Execute HealthKit observer query for loading increment samples")
  415. guard observerError == nil else {
  416. warning(.service, "Error during execution of HealthKit Observer's query", error: observerError!)
  417. return
  418. }
  419. if let incrementQuery = self.getBloodGlucoseHKQuery(predicate: self.loadBGPredicate) {
  420. debug(.service, "Create increment query")
  421. self.healthKitStore.execute(incrementQuery)
  422. }
  423. }
  424. healthKitStore.execute(query)
  425. debug(.service, "Create Observer for Blood Glucose")
  426. }
  427. func enableBackgroundDelivery() {
  428. guard settingsManager.settings.useAppleHealth else {
  429. healthKitStore.disableAllBackgroundDelivery { _, _ in }
  430. return }
  431. guard let bgType = Config.healthBGObject else {
  432. warning(
  433. .service,
  434. "Can not create background delivery, because unable to get the Blood Glucose type"
  435. )
  436. return
  437. }
  438. healthKitStore.enableBackgroundDelivery(for: bgType, frequency: .immediate) { status, error in
  439. guard error == nil else {
  440. warning(.service, "Can not enable background delivery", error: error)
  441. return
  442. }
  443. debug(.service, "Background delivery status is \(status)")
  444. }
  445. }
  446. /// Try to load samples from Health store
  447. private func loadSamplesFromHealth(
  448. sampleType: HKQuantityType,
  449. limit: Int = 100,
  450. completion: @escaping (_ samples: [HKSample]) -> Void
  451. ) {
  452. let query = HKSampleQuery(
  453. sampleType: sampleType,
  454. predicate: nil,
  455. limit: limit,
  456. sortDescriptors: nil
  457. ) { _, results, _ in
  458. completion(results as? [HKQuantitySample] ?? [])
  459. }
  460. healthKitStore.execute(query)
  461. }
  462. /// Try to load samples from Health store with id and do some work
  463. private func loadSamplesFromHealth(
  464. sampleType: HKQuantityType,
  465. withIDs ids: [String],
  466. limit: Int = 100,
  467. completion: @escaping (_ samples: [HKSample]) -> Void
  468. ) {
  469. let predicate = HKQuery.predicateForObjects(
  470. withMetadataKey: HKMetadataKeySyncIdentifier,
  471. allowedValues: ids
  472. )
  473. let query = HKSampleQuery(
  474. sampleType: sampleType,
  475. predicate: predicate,
  476. limit: limit,
  477. sortDescriptors: nil
  478. ) { _, results, _ in
  479. completion(results as? [HKQuantitySample] ?? [])
  480. }
  481. healthKitStore.execute(query)
  482. }
  483. private func getBloodGlucoseHKQuery(predicate: NSPredicate) -> HKQuery? {
  484. guard let sampleType = Config.healthBGObject else { return nil }
  485. let query = HKAnchoredObjectQuery(
  486. type: sampleType,
  487. predicate: predicate,
  488. anchor: lastBloodGlucoseQueryAnchor,
  489. limit: HKObjectQueryNoLimit
  490. ) { [weak self] _, addedObjects, _, anchor, _ in
  491. guard let self = self else { return }
  492. self.processQueue.async {
  493. debug(.service, "AnchoredQuery did execute")
  494. self.lastBloodGlucoseQueryAnchor = anchor
  495. // Added objects
  496. if let bgSamples = addedObjects as? [HKQuantitySample],
  497. bgSamples.isNotEmpty
  498. {
  499. self.prepareBGSamplesToPublisherFetch(bgSamples)
  500. }
  501. }
  502. }
  503. return query
  504. }
  505. private func prepareBGSamplesToPublisherFetch(_ samples: [HKQuantitySample]) {
  506. dispatchPrecondition(condition: .onQueue(processQueue))
  507. newGlucose += samples
  508. .compactMap { sample -> HealthKitSample? in
  509. let fromTrio = sample.metadata?[Config.freeAPSMetaKey] as? Bool ?? false
  510. guard !fromTrio else { return nil }
  511. return HealthKitSample(
  512. healthKitId: sample.uuid.uuidString,
  513. date: sample.startDate,
  514. glucose: Int(round(sample.quantity.doubleValue(for: .milligramsPerDeciliter)))
  515. )
  516. }
  517. .map { sample in
  518. BloodGlucose(
  519. _id: sample.healthKitId,
  520. sgv: sample.glucose,
  521. direction: nil,
  522. date: Decimal(Int(sample.date.timeIntervalSince1970) * 1000),
  523. dateString: sample.date,
  524. unfiltered: Decimal(sample.glucose),
  525. filtered: nil,
  526. noise: nil,
  527. glucose: sample.glucose,
  528. type: "sgv"
  529. )
  530. }
  531. .filter { $0.dateString >= Date().addingTimeInterval(-1.days.timeInterval) }
  532. newGlucose = newGlucose.removeDublicates()
  533. }
  534. // MARK: - GlucoseSource
  535. var glucoseManager: FetchGlucoseManager?
  536. var cgmManager: CGMManagerUI?
  537. func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
  538. Future { [weak self] promise in
  539. guard let self = self else {
  540. promise(.success([]))
  541. return
  542. }
  543. self.processQueue.async {
  544. guard self.settingsManager.settings.useAppleHealth else {
  545. promise(.success([]))
  546. return
  547. }
  548. // Remove old BGs
  549. self.newGlucose = self.newGlucose
  550. .filter { $0.dateString >= Date().addingTimeInterval(-1.days.timeInterval) }
  551. // Get actual BGs (beetwen Date() - 1 day and Date())
  552. let actualGlucose = self.newGlucose
  553. .filter { $0.dateString <= Date() }
  554. // Update newGlucose
  555. self.newGlucose = self.newGlucose
  556. .filter { !actualGlucose.contains($0) }
  557. // debug(.service, "Actual glucose is \(actualGlucose)")
  558. // debug(.service, "Current state of newGlucose is \(self.newGlucose)")
  559. promise(.success(actualGlucose))
  560. }
  561. }
  562. .eraseToAnyPublisher()
  563. }
  564. func fetchIfNeeded() -> AnyPublisher<[BloodGlucose], Never> {
  565. fetch(nil)
  566. }
  567. func deleteGlucose(syncID: String) {
  568. guard settingsManager.settings.useAppleHealth,
  569. let sampleType = Config.healthBGObject,
  570. checkAvailabilitySave(objectTypeToHealthStore: sampleType)
  571. else { return }
  572. processQueue.async {
  573. let predicate = HKQuery.predicateForObjects(
  574. withMetadataKey: HKMetadataKeySyncIdentifier,
  575. operatorType: .equalTo,
  576. value: syncID
  577. )
  578. self.healthKitStore.deleteObjects(of: sampleType, predicate: predicate) { _, _, error in
  579. guard let error = error else { return }
  580. warning(.service, "Cannot delete sample with syncID: \(syncID)", error: error)
  581. }
  582. }
  583. }
  584. // - MARK Carbs function
  585. func deleteCarbs(syncID: String, fpuID: String) {
  586. guard settingsManager.settings.useAppleHealth,
  587. let sampleType = Config.healthCarbObject,
  588. checkAvailabilitySave(objectTypeToHealthStore: sampleType)
  589. else { return }
  590. print("meals 4: ID: " + syncID + " FPU ID: " + fpuID)
  591. if syncID != "" {
  592. let predicate = HKQuery.predicateForObjects(
  593. withMetadataKey: HKMetadataKeySyncIdentifier,
  594. operatorType: .equalTo,
  595. value: syncID
  596. )
  597. healthKitStore.deleteObjects(of: sampleType, predicate: predicate) { _, _, error in
  598. guard let error = error else { return }
  599. warning(.service, "Cannot delete sample with syncID: \(syncID)", error: error)
  600. }
  601. }
  602. if fpuID != "" {
  603. // processQueue.async {
  604. let recentCarbs: [CarbsEntry] = carbsStorage.recent()
  605. let ids = recentCarbs.filter { $0.fpuID == fpuID }.compactMap(\.id)
  606. let predicate = HKQuery.predicateForObjects(
  607. withMetadataKey: HKMetadataKeySyncIdentifier,
  608. allowedValues: ids
  609. )
  610. print("found IDs: " + ids.description)
  611. healthKitStore.deleteObjects(of: sampleType, predicate: predicate) { _, _, error in
  612. guard let error = error else { return }
  613. warning(.service, "Cannot delete sample with fpuID: \(fpuID)", error: error)
  614. }
  615. // }
  616. }
  617. }
  618. func carbsDidUpdate(_ carbs: [CarbsEntry]) {
  619. saveIfNeeded(carbs: carbs)
  620. }
  621. // - MARK Insulin function
  622. func deleteInsulin(syncID: String) {
  623. guard settingsManager.settings.useAppleHealth,
  624. let sampleType = Config.healthInsulinObject,
  625. checkAvailabilitySave(objectTypeToHealthStore: sampleType)
  626. else { return }
  627. processQueue.async {
  628. let predicate = HKQuery.predicateForObjects(
  629. withMetadataKey: HKMetadataKeySyncIdentifier,
  630. operatorType: .equalTo,
  631. value: syncID
  632. )
  633. self.healthKitStore.deleteObjects(of: sampleType, predicate: predicate) { _, _, error in
  634. guard let error = error else { return }
  635. warning(.service, "Cannot delete sample with syncID: \(syncID)", error: error)
  636. }
  637. }
  638. }
  639. }
  640. enum HealthKitPermissionRequestStatus {
  641. case needRequest
  642. case didRequest
  643. }
  644. enum HKError: Error {
  645. // HealthKit work only iPhone (not on iPad)
  646. case notAvailableOnCurrentDevice
  647. // Some data can be not available on current iOS-device
  648. case dataNotAvailable
  649. }
  650. private struct InsulinBolus {
  651. var id: String
  652. var amount: Decimal
  653. var date: Date
  654. }
  655. private struct InsulinBasal {
  656. var id: String
  657. var amount: Decimal
  658. var startDelivery: Date
  659. var endDelivery: Date
  660. }