FetchGlucoseManager.swift 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  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 healthKitManager: HealthKitManager!
  21. @Injected() var deviceDataManager: DeviceDataManager!
  22. private var lifetime = Lifetime()
  23. private let timer = DispatchTimer(timeInterval: 1.minutes.timeInterval)
  24. var cgmGlucoseSourceType: CGMType?
  25. private lazy var dexcomSourceG5 = DexcomSourceG5(glucoseStorage: glucoseStorage, glucoseManager: self)
  26. private lazy var dexcomSourceG6 = DexcomSourceG6(glucoseStorage: glucoseStorage, glucoseManager: self)
  27. private lazy var dexcomSourceG7 = DexcomSourceG7(glucoseStorage: glucoseStorage, glucoseManager: self)
  28. private lazy var libreSource = LibreTransmitterSource(glucoseStorage: glucoseStorage, glucoseManager: self)
  29. private lazy var simulatorSource = GlucoseSimulatorSource()
  30. init(resolver: Resolver) {
  31. injectServices(resolver)
  32. updateGlucoseSource()
  33. subscribe()
  34. }
  35. var glucoseSource: GlucoseSource!
  36. func updateGlucoseSource() {
  37. switch settingsManager.settings.cgm {
  38. case .xdrip:
  39. glucoseSource = AppGroupSource(from: "xDrip", cgmType: .xdrip)
  40. case .dexcomG5:
  41. glucoseSource = dexcomSourceG5
  42. case .dexcomG6:
  43. glucoseSource = dexcomSourceG6
  44. case .dexcomG7:
  45. glucoseSource = dexcomSourceG7
  46. case .nightscout:
  47. glucoseSource = nightscoutManager
  48. case .simulator:
  49. glucoseSource = simulatorSource
  50. case .libreTransmitter:
  51. glucoseSource = libreSource
  52. case .glucoseDirect:
  53. glucoseSource = AppGroupSource(from: "GlucoseDirect", cgmType: .glucoseDirect)
  54. case .enlite:
  55. glucoseSource = deviceDataManager
  56. }
  57. // update the config
  58. cgmGlucoseSourceType = settingsManager.settings.cgm
  59. }
  60. /// function called when a callback is fired by CGM BLE - no more used
  61. public func updateGlucoseStore(newBloodGlucose: [BloodGlucose]) {
  62. let syncDate = glucoseStorage.syncDate()
  63. debug(.deviceManager, "CGM BLE FETCHGLUCOSE : SyncDate is \(syncDate)")
  64. glucoseStoreAndHeartDecision(syncDate: syncDate, glucose: newBloodGlucose)
  65. }
  66. /// function to try to force the refresh of the CGM - generally provide by the pump heartbeat
  67. public func refreshCGM() {
  68. debug(.deviceManager, "refreshCGM by pump")
  69. updateGlucoseSource()
  70. Publishers.CombineLatest3(
  71. Just(glucoseStorage.syncDate()),
  72. healthKitManager.fetch(nil),
  73. glucoseSource.fetchIfNeeded()
  74. )
  75. .eraseToAnyPublisher()
  76. .receive(on: processQueue)
  77. .sink { syncDate, glucoseFromHealth, glucose in
  78. debug(.nightscout, "refreshCGM FETCHGLUCOSE : SyncDate is \(syncDate)")
  79. self.glucoseStoreAndHeartDecision(syncDate: syncDate, glucose: glucose, glucoseFromHealth: glucoseFromHealth)
  80. }
  81. .store(in: &lifetime)
  82. }
  83. private func glucoseStoreAndHeartDecision(syncDate: Date, glucose: [BloodGlucose], glucoseFromHealth: [BloodGlucose] = []) {
  84. let allGlucose = glucose + glucoseFromHealth
  85. var filteredByDate: [BloodGlucose] = []
  86. var filtered: [BloodGlucose] = []
  87. // start background time extension
  88. var backGroundFetchBGTaskID: UIBackgroundTaskIdentifier?
  89. backGroundFetchBGTaskID = UIApplication.shared.beginBackgroundTask(withName: "save BG starting") {
  90. guard let bg = backGroundFetchBGTaskID else { return }
  91. UIApplication.shared.endBackgroundTask(bg)
  92. backGroundFetchBGTaskID = .invalid
  93. }
  94. guard allGlucose.isNotEmpty else {
  95. if let backgroundTask = backGroundFetchBGTaskID {
  96. UIApplication.shared.endBackgroundTask(backgroundTask)
  97. backGroundFetchBGTaskID = .invalid
  98. }
  99. return
  100. }
  101. filteredByDate = allGlucose.filter { $0.dateString > syncDate }
  102. filtered = glucoseStorage.filterTooFrequentGlucose(filteredByDate, at: syncDate)
  103. guard filtered.isNotEmpty else {
  104. // end of the BG tasks
  105. if let backgroundTask = backGroundFetchBGTaskID {
  106. UIApplication.shared.endBackgroundTask(backgroundTask)
  107. backGroundFetchBGTaskID = .invalid
  108. }
  109. return
  110. }
  111. debug(.deviceManager, "New glucose found")
  112. // filter the data if it is the case
  113. if settingsManager.settings.smoothGlucose {
  114. // limit to 30 minutes of previous BG Data
  115. let oldGlucoses = glucoseStorage.recent().filter {
  116. $0.dateString.addingTimeInterval(31 * 60) > Date()
  117. }
  118. var smoothedValues = oldGlucoses + filtered
  119. // smooth with 3 repeats
  120. for _ in 1 ... 3 {
  121. smoothedValues.smoothSavitzkyGolayQuaDratic(withFilterWidth: 3)
  122. }
  123. // find the new values only
  124. filtered = smoothedValues.filter { $0.dateString > syncDate }
  125. }
  126. glucoseStorage.storeGlucose(filtered)
  127. deviceDataManager.heartbeat(date: Date())
  128. nightscoutManager.uploadGlucose()
  129. let glucoseForHealth = filteredByDate.filter { !glucoseFromHealth.contains($0) }
  130. guard glucoseForHealth.isNotEmpty else {
  131. // end of the BG tasks
  132. if let backgroundTask = backGroundFetchBGTaskID {
  133. UIApplication.shared.endBackgroundTask(backgroundTask)
  134. backGroundFetchBGTaskID = .invalid
  135. }
  136. return
  137. }
  138. healthKitManager.saveIfNeeded(bloodGlucose: glucoseForHealth)
  139. // end of the BG tasks
  140. if let backgroundTask = backGroundFetchBGTaskID {
  141. UIApplication.shared.endBackgroundTask(backgroundTask)
  142. backGroundFetchBGTaskID = .invalid
  143. }
  144. }
  145. /// The function used to start the timer sync - Function of the variable defined in config
  146. private func subscribe() {
  147. timer.publisher
  148. .receive(on: processQueue)
  149. .flatMap { _ -> AnyPublisher<[BloodGlucose], Never> in
  150. debug(.nightscout, "FetchGlucoseManager timer heartbeat")
  151. self.updateGlucoseSource()
  152. return self.glucoseSource.fetch(self.timer).eraseToAnyPublisher()
  153. }
  154. .sink { glucose in
  155. debug(.nightscout, "FetchGlucoseManager callback sensor")
  156. Publishers.CombineLatest3(
  157. Just(glucose),
  158. Just(self.glucoseStorage.syncDate()),
  159. self.healthKitManager.fetch(nil)
  160. )
  161. .eraseToAnyPublisher()
  162. .sink { newGlucose, syncDate, glucoseFromHealth in
  163. self.glucoseStoreAndHeartDecision(
  164. syncDate: syncDate,
  165. glucose: newGlucose,
  166. glucoseFromHealth: glucoseFromHealth
  167. )
  168. }
  169. .store(in: &self.lifetime)
  170. }
  171. .store(in: &lifetime)
  172. timer.fire()
  173. timer.resume()
  174. UserDefaults.standard
  175. .publisher(for: \.dexcomTransmitterID)
  176. .removeDuplicates()
  177. .sink { id in
  178. if self.settingsManager.settings.cgm == .dexcomG5 {
  179. if id != self.dexcomSourceG5.transmitterID {
  180. self.dexcomSourceG5 = DexcomSourceG5(glucoseStorage: self.glucoseStorage, glucoseManager: self)
  181. }
  182. } else if self.settingsManager.settings.cgm == .dexcomG6 {
  183. if id != self.dexcomSourceG6.transmitterID {
  184. self.dexcomSourceG6 = DexcomSourceG6(glucoseStorage: self.glucoseStorage, glucoseManager: self)
  185. }
  186. }
  187. }
  188. .store(in: &lifetime)
  189. }
  190. func sourceInfo() -> [String: Any]? {
  191. glucoseSource.sourceInfo()
  192. }
  193. }
  194. extension UserDefaults {
  195. @objc var dexcomTransmitterID: String? {
  196. get {
  197. string(forKey: "DexcomSource.transmitterID")?.nonEmpty
  198. }
  199. set {
  200. set(newValue, forKey: "DexcomSource.transmitterID")
  201. }
  202. }
  203. }