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. private let context = CoreDataStack.shared.newTaskContext()
  31. init(resolver: Resolver) {
  32. injectServices(resolver)
  33. updateGlucoseSource()
  34. subscribe()
  35. }
  36. var glucoseSource: GlucoseSource!
  37. func updateGlucoseSource() {
  38. switch settingsManager.settings.cgm {
  39. case .xdrip:
  40. glucoseSource = AppGroupSource(from: "xDrip", cgmType: .xdrip)
  41. case .dexcomG5:
  42. glucoseSource = dexcomSourceG5
  43. case .dexcomG6:
  44. glucoseSource = dexcomSourceG6
  45. case .dexcomG7:
  46. glucoseSource = dexcomSourceG7
  47. case .nightscout:
  48. glucoseSource = nightscoutManager
  49. case .simulator:
  50. glucoseSource = simulatorSource
  51. case .libreTransmitter:
  52. glucoseSource = libreTransmitter
  53. case .glucoseDirect:
  54. glucoseSource = AppGroupSource(from: "GlucoseDirect", cgmType: .glucoseDirect)
  55. case .enlite:
  56. glucoseSource = deviceDataManager
  57. }
  58. // update the config
  59. cgmGlucoseSourceType = settingsManager.settings.cgm
  60. if settingsManager.settings.cgm != .libreTransmitter {
  61. libreTransmitter.manager = nil
  62. } else {
  63. libreTransmitter.glucoseManager = self
  64. }
  65. }
  66. /// function called when a callback is fired by CGM BLE - no more used
  67. public func updateGlucoseStore(newBloodGlucose: [BloodGlucose]) {
  68. let syncDate = glucoseStorage.syncDate()
  69. debug(.deviceManager, "CGM BLE FETCHGLUCOSE : SyncDate is \(syncDate)")
  70. glucoseStoreAndHeartDecision(syncDate: syncDate, glucose: newBloodGlucose)
  71. }
  72. /// function to try to force the refresh of the CGM - generally provide by the pump heartbeat
  73. public func refreshCGM() {
  74. debug(.deviceManager, "refreshCGM by pump")
  75. updateGlucoseSource()
  76. Publishers.CombineLatest3(
  77. Just(glucoseStorage.syncDate()),
  78. healthKitManager.fetch(nil),
  79. glucoseSource.fetchIfNeeded()
  80. )
  81. .eraseToAnyPublisher()
  82. .receive(on: processQueue)
  83. .sink { syncDate, glucoseFromHealth, glucose in
  84. debug(.nightscout, "refreshCGM FETCHGLUCOSE : SyncDate is \(syncDate)")
  85. self.glucoseStoreAndHeartDecision(syncDate: syncDate, glucose: glucose, glucoseFromHealth: glucoseFromHealth)
  86. }
  87. .store(in: &lifetime)
  88. }
  89. private func fetchGlucose() -> [GlucoseStored]? {
  90. CoreDataStack.shared.fetchEntities(
  91. ofType: GlucoseStored.self,
  92. onContext: context,
  93. predicate: NSPredicate.predicateFor30MinAgo,
  94. key: "date",
  95. ascending: false,
  96. fetchLimit: 6
  97. )
  98. }
  99. private func processGlucose() -> [BloodGlucose] {
  100. context.performAndWait {
  101. guard let results = fetchGlucose() else { return [] }
  102. return results.map { result in
  103. BloodGlucose(
  104. sgv: Int(result.glucose),
  105. direction: BloodGlucose.Direction(from: result.direction ?? ""),
  106. date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
  107. dateString: result.date ?? Date(),
  108. unfiltered: Decimal(result.glucose),
  109. filtered: Decimal(result.glucose),
  110. noise: nil,
  111. glucose: Int(result.glucose)
  112. )
  113. }
  114. }
  115. }
  116. private func glucoseStoreAndHeartDecision(syncDate: Date, glucose: [BloodGlucose], glucoseFromHealth: [BloodGlucose] = []) {
  117. let allGlucose = glucose + glucoseFromHealth
  118. var filteredByDate: [BloodGlucose] = []
  119. var filtered: [BloodGlucose] = []
  120. // start background time extension
  121. var backGroundFetchBGTaskID: UIBackgroundTaskIdentifier?
  122. backGroundFetchBGTaskID = UIApplication.shared.beginBackgroundTask(withName: "save BG starting") {
  123. guard let bg = backGroundFetchBGTaskID else { return }
  124. UIApplication.shared.endBackgroundTask(bg)
  125. backGroundFetchBGTaskID = .invalid
  126. }
  127. guard allGlucose.isNotEmpty else {
  128. if let backgroundTask = backGroundFetchBGTaskID {
  129. UIApplication.shared.endBackgroundTask(backgroundTask)
  130. backGroundFetchBGTaskID = .invalid
  131. }
  132. return
  133. }
  134. filteredByDate = allGlucose.filter { $0.dateString > syncDate }
  135. filtered = glucoseStorage.filterTooFrequentGlucose(filteredByDate, at: syncDate)
  136. guard filtered.isNotEmpty else {
  137. // end of the BG tasks
  138. if let backgroundTask = backGroundFetchBGTaskID {
  139. UIApplication.shared.endBackgroundTask(backgroundTask)
  140. backGroundFetchBGTaskID = .invalid
  141. }
  142. return
  143. }
  144. debug(.deviceManager, "New glucose found")
  145. // filter the data if it is the case
  146. if settingsManager.settings.smoothGlucose {
  147. // limited to 30 min of old glucose data
  148. let oldGlucoseValues = processGlucose()
  149. var smoothedValues = oldGlucoseValues + filtered
  150. // smooth with 3 repeats
  151. for _ in 1 ... 3 {
  152. smoothedValues.smoothSavitzkyGolayQuaDratic(withFilterWidth: 3)
  153. }
  154. // find the new values only
  155. filtered = smoothedValues.filter { $0.dateString > syncDate }
  156. }
  157. glucoseStorage.storeGlucose(filtered)
  158. deviceDataManager.heartbeat(date: Date())
  159. Task.detached {
  160. await self.nightscoutManager.uploadGlucose()
  161. }
  162. let glucoseForHealth = filteredByDate.filter { !glucoseFromHealth.contains($0) }
  163. guard glucoseForHealth.isNotEmpty else {
  164. // end of the BG tasks
  165. if let backgroundTask = backGroundFetchBGTaskID {
  166. UIApplication.shared.endBackgroundTask(backgroundTask)
  167. backGroundFetchBGTaskID = .invalid
  168. }
  169. return
  170. }
  171. healthKitManager.saveIfNeeded(bloodGlucose: glucoseForHealth)
  172. // end of the BG tasks
  173. if let backgroundTask = backGroundFetchBGTaskID {
  174. UIApplication.shared.endBackgroundTask(backgroundTask)
  175. backGroundFetchBGTaskID = .invalid
  176. }
  177. }
  178. /// The function used to start the timer sync - Function of the variable defined in config
  179. private func subscribe() {
  180. timer.publisher
  181. .receive(on: processQueue)
  182. .flatMap { _ -> AnyPublisher<[BloodGlucose], Never> in
  183. debug(.nightscout, "FetchGlucoseManager timer heartbeat")
  184. self.updateGlucoseSource()
  185. return self.glucoseSource.fetch(self.timer).eraseToAnyPublisher()
  186. }
  187. .sink { glucose in
  188. debug(.nightscout, "FetchGlucoseManager callback sensor")
  189. Publishers.CombineLatest3(
  190. Just(glucose),
  191. Just(self.glucoseStorage.syncDate()),
  192. self.healthKitManager.fetch(nil)
  193. )
  194. .eraseToAnyPublisher()
  195. .sink { newGlucose, syncDate, glucoseFromHealth in
  196. self.glucoseStoreAndHeartDecision(
  197. syncDate: syncDate,
  198. glucose: newGlucose,
  199. glucoseFromHealth: glucoseFromHealth
  200. )
  201. }
  202. .store(in: &self.lifetime)
  203. }
  204. .store(in: &lifetime)
  205. timer.fire()
  206. timer.resume()
  207. UserDefaults.standard
  208. .publisher(for: \.dexcomTransmitterID)
  209. .removeDuplicates()
  210. .sink { id in
  211. if self.settingsManager.settings.cgm == .dexcomG5 {
  212. if id != self.dexcomSourceG5.transmitterID {
  213. self.dexcomSourceG5 = DexcomSourceG5(glucoseStorage: self.glucoseStorage, glucoseManager: self)
  214. }
  215. } else if self.settingsManager.settings.cgm == .dexcomG6 {
  216. if id != self.dexcomSourceG6.transmitterID {
  217. self.dexcomSourceG6 = DexcomSourceG6(glucoseStorage: self.glucoseStorage, glucoseManager: self)
  218. }
  219. }
  220. }
  221. .store(in: &lifetime)
  222. }
  223. func sourceInfo() -> [String: Any]? {
  224. glucoseSource.sourceInfo()
  225. }
  226. }
  227. extension UserDefaults {
  228. @objc var dexcomTransmitterID: String? {
  229. get {
  230. string(forKey: "DexcomSource.transmitterID")?.nonEmpty
  231. }
  232. set {
  233. set(newValue, forKey: "DexcomSource.transmitterID")
  234. }
  235. }
  236. }