FetchGlucoseManager.swift 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. import Combine
  2. import Foundation
  3. import SwiftDate
  4. import Swinject
  5. import UIKit
  6. protocol FetchGlucoseManager: SourceInfoProvider {
  7. func updateGlucoseStore(newBloodGlucose: [BloodGlucose])
  8. func refreshCGM()
  9. func updateGlucoseSource()
  10. var glucoseSource: GlucoseSource! { get }
  11. var cgmGlucoseSourceType: CGMType? { get set }
  12. var settingsManager: SettingsManager! { get }
  13. }
  14. final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
  15. private let processQueue = DispatchQueue(label: "BaseGlucoseManager.processQueue")
  16. @Injected() var glucoseStorage: GlucoseStorage!
  17. @Injected() var nightscoutManager: NightscoutManager!
  18. @Injected() var apsManager: APSManager!
  19. @Injected() var settingsManager: SettingsManager!
  20. @Injected() var libreTransmitter: LibreTransmitterSource!
  21. @Injected() var healthKitManager: HealthKitManager!
  22. @Injected() var deviceDataManager: DeviceDataManager!
  23. private var lifetime = Lifetime()
  24. private let timer = DispatchTimer(timeInterval: 1.minutes.timeInterval)
  25. var cgmGlucoseSourceType: CGMType?
  26. private lazy var dexcomSourceG5 = DexcomSourceG5(glucoseStorage: glucoseStorage, glucoseManager: self)
  27. private lazy var dexcomSourceG6 = DexcomSourceG6(glucoseStorage: glucoseStorage, glucoseManager: self)
  28. private lazy var dexcomSourceG7 = DexcomSourceG7(glucoseStorage: glucoseStorage, glucoseManager: self)
  29. private lazy var simulatorSource = GlucoseSimulatorSource()
  30. // TODO: - test if we need to use the viewContext here
  31. private let context = CoreDataStack.shared.backgroundContext
  32. init(resolver: Resolver) {
  33. injectServices(resolver)
  34. updateGlucoseSource()
  35. subscribe()
  36. }
  37. var glucoseSource: GlucoseSource!
  38. func updateGlucoseSource() {
  39. switch settingsManager.settings.cgm {
  40. case .xdrip:
  41. glucoseSource = AppGroupSource(from: "xDrip", cgmType: .xdrip)
  42. case .dexcomG5:
  43. glucoseSource = dexcomSourceG5
  44. case .dexcomG6:
  45. glucoseSource = dexcomSourceG6
  46. case .dexcomG7:
  47. glucoseSource = dexcomSourceG7
  48. case .nightscout:
  49. glucoseSource = nightscoutManager
  50. case .simulator:
  51. glucoseSource = simulatorSource
  52. case .libreTransmitter:
  53. glucoseSource = libreTransmitter
  54. case .glucoseDirect:
  55. glucoseSource = AppGroupSource(from: "GlucoseDirect", cgmType: .glucoseDirect)
  56. case .enlite:
  57. glucoseSource = deviceDataManager
  58. }
  59. // update the config
  60. cgmGlucoseSourceType = settingsManager.settings.cgm
  61. if settingsManager.settings.cgm != .libreTransmitter {
  62. libreTransmitter.manager = nil
  63. } else {
  64. libreTransmitter.glucoseManager = self
  65. }
  66. }
  67. /// function called when a callback is fired by CGM BLE - no more used
  68. public func updateGlucoseStore(newBloodGlucose: [BloodGlucose]) {
  69. let syncDate = glucoseStorage.syncDate()
  70. debug(.deviceManager, "CGM BLE FETCHGLUCOSE : SyncDate is \(syncDate)")
  71. glucoseStoreAndHeartDecision(syncDate: syncDate, glucose: newBloodGlucose)
  72. }
  73. /// function to try to force the refresh of the CGM - generally provide by the pump heartbeat
  74. public func refreshCGM() {
  75. debug(.deviceManager, "refreshCGM by pump")
  76. updateGlucoseSource()
  77. Publishers.CombineLatest3(
  78. Just(glucoseStorage.syncDate()),
  79. healthKitManager.fetch(nil),
  80. glucoseSource.fetchIfNeeded()
  81. )
  82. .eraseToAnyPublisher()
  83. .receive(on: processQueue)
  84. .sink { syncDate, glucoseFromHealth, glucose in
  85. debug(.nightscout, "refreshCGM FETCHGLUCOSE : SyncDate is \(syncDate)")
  86. self.glucoseStoreAndHeartDecision(syncDate: syncDate, glucose: glucose, glucoseFromHealth: glucoseFromHealth)
  87. }
  88. .store(in: &lifetime)
  89. }
  90. private func fetchAndProcessGlucose() -> [BloodGlucose] {
  91. do {
  92. let results = try context.fetch(GlucoseStored.fetch(
  93. NSPredicate.predicateFor30MinAgo,
  94. ascending: false,
  95. fetchLimit: 6
  96. ))
  97. debugPrint("Fetch Glucose Manager: \(#function) \(DebuggingIdentifiers.succeeded) fetched glucose")
  98. var glucoseArray = [BloodGlucose]()
  99. for result in results {
  100. // TODO: - when parsing the CD object to JSON we currently don't have a direction
  101. let glucose = BloodGlucose(
  102. date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
  103. dateString: result.date ?? Date(),
  104. unfiltered: Decimal(result.glucose),
  105. filtered: Decimal(result.glucose),
  106. noise: nil,
  107. type: ""
  108. )
  109. glucoseArray.append(glucose)
  110. }
  111. return glucoseArray
  112. } catch {
  113. debugPrint("Fetch Glucose Manager: \(#function) \(DebuggingIdentifiers.failed) failed to fetch glucose")
  114. return []
  115. }
  116. }
  117. private func glucoseStoreAndHeartDecision(syncDate: Date, glucose: [BloodGlucose], glucoseFromHealth: [BloodGlucose] = []) {
  118. let allGlucose = glucose + glucoseFromHealth
  119. var filteredByDate: [BloodGlucose] = []
  120. var filtered: [BloodGlucose] = []
  121. // start background time extension
  122. var backGroundFetchBGTaskID: UIBackgroundTaskIdentifier?
  123. backGroundFetchBGTaskID = UIApplication.shared.beginBackgroundTask(withName: "save BG starting") {
  124. guard let bg = backGroundFetchBGTaskID else { return }
  125. UIApplication.shared.endBackgroundTask(bg)
  126. backGroundFetchBGTaskID = .invalid
  127. }
  128. guard allGlucose.isNotEmpty else {
  129. if let backgroundTask = backGroundFetchBGTaskID {
  130. UIApplication.shared.endBackgroundTask(backgroundTask)
  131. backGroundFetchBGTaskID = .invalid
  132. }
  133. return
  134. }
  135. filteredByDate = allGlucose.filter { $0.dateString > syncDate }
  136. filtered = glucoseStorage.filterTooFrequentGlucose(filteredByDate, at: syncDate)
  137. guard filtered.isNotEmpty else {
  138. // end of the BG tasks
  139. if let backgroundTask = backGroundFetchBGTaskID {
  140. UIApplication.shared.endBackgroundTask(backgroundTask)
  141. backGroundFetchBGTaskID = .invalid
  142. }
  143. return
  144. }
  145. debug(.deviceManager, "New glucose found")
  146. // filter the data if it is the case
  147. if settingsManager.settings.smoothGlucose {
  148. // limited to 30 min of old glucose data
  149. let oldGlucoseValues = fetchAndProcessGlucose()
  150. var smoothedValues = oldGlucoseValues + filtered
  151. // smooth with 3 repeats
  152. for _ in 1 ... 3 {
  153. smoothedValues.smoothSavitzkyGolayQuaDratic(withFilterWidth: 3)
  154. }
  155. // find the new values only
  156. filtered = smoothedValues.filter { $0.dateString > syncDate }
  157. }
  158. glucoseStorage.storeGlucose(filtered)
  159. deviceDataManager.heartbeat(date: Date())
  160. nightscoutManager.uploadGlucose()
  161. let glucoseForHealth = filteredByDate.filter { !glucoseFromHealth.contains($0) }
  162. guard glucoseForHealth.isNotEmpty else {
  163. // end of the BG tasks
  164. if let backgroundTask = backGroundFetchBGTaskID {
  165. UIApplication.shared.endBackgroundTask(backgroundTask)
  166. backGroundFetchBGTaskID = .invalid
  167. }
  168. return
  169. }
  170. healthKitManager.saveIfNeeded(bloodGlucose: glucoseForHealth)
  171. // end of the BG tasks
  172. if let backgroundTask = backGroundFetchBGTaskID {
  173. UIApplication.shared.endBackgroundTask(backgroundTask)
  174. backGroundFetchBGTaskID = .invalid
  175. }
  176. }
  177. /// The function used to start the timer sync - Function of the variable defined in config
  178. private func subscribe() {
  179. timer.publisher
  180. .receive(on: processQueue)
  181. .flatMap { _ -> AnyPublisher<[BloodGlucose], Never> in
  182. debug(.nightscout, "FetchGlucoseManager timer heartbeat")
  183. self.updateGlucoseSource()
  184. return self.glucoseSource.fetch(self.timer).eraseToAnyPublisher()
  185. }
  186. .sink { glucose in
  187. debug(.nightscout, "FetchGlucoseManager callback sensor")
  188. Publishers.CombineLatest3(
  189. Just(glucose),
  190. Just(self.glucoseStorage.syncDate()),
  191. self.healthKitManager.fetch(nil)
  192. )
  193. .eraseToAnyPublisher()
  194. .sink { newGlucose, syncDate, glucoseFromHealth in
  195. self.glucoseStoreAndHeartDecision(
  196. syncDate: syncDate,
  197. glucose: newGlucose,
  198. glucoseFromHealth: glucoseFromHealth
  199. )
  200. }
  201. .store(in: &self.lifetime)
  202. }
  203. .store(in: &lifetime)
  204. timer.fire()
  205. timer.resume()
  206. UserDefaults.standard
  207. .publisher(for: \.dexcomTransmitterID)
  208. .removeDuplicates()
  209. .sink { id in
  210. if self.settingsManager.settings.cgm == .dexcomG5 {
  211. if id != self.dexcomSourceG5.transmitterID {
  212. self.dexcomSourceG5 = DexcomSourceG5(glucoseStorage: self.glucoseStorage, glucoseManager: self)
  213. }
  214. } else if self.settingsManager.settings.cgm == .dexcomG6 {
  215. if id != self.dexcomSourceG6.transmitterID {
  216. self.dexcomSourceG6 = DexcomSourceG6(glucoseStorage: self.glucoseStorage, glucoseManager: self)
  217. }
  218. }
  219. }
  220. .store(in: &lifetime)
  221. }
  222. func sourceInfo() -> [String: Any]? {
  223. glucoseSource.sourceInfo()
  224. }
  225. }
  226. extension UserDefaults {
  227. @objc var dexcomTransmitterID: String? {
  228. get {
  229. string(forKey: "DexcomSource.transmitterID")?.nonEmpty
  230. }
  231. set {
  232. set(newValue, forKey: "DexcomSource.transmitterID")
  233. }
  234. }
  235. }