FetchGlucoseManager.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. import Combine
  2. import Foundation
  3. import LoopKit
  4. import LoopKitUI
  5. import SwiftDate
  6. import Swinject
  7. import UIKit
  8. protocol FetchGlucoseManager: SourceInfoProvider {
  9. func updateGlucoseStore(newBloodGlucose: [BloodGlucose])
  10. func refreshCGM()
  11. func updateGlucoseSource(cgmGlucoseSourceType: CGMType, cgmGlucosePluginId: String, newManager: CGMManagerUI?)
  12. func deleteGlucoseSource()
  13. func removeCalibrations()
  14. var glucoseSource: GlucoseSource! { get }
  15. var cgmManager: CGMManagerUI? { get }
  16. var cgmGlucoseSourceType: CGMType? { get set }
  17. var cgmGlucosePluginId: String? { get }
  18. var settingsManager: SettingsManager! { get }
  19. var shouldSyncToRemoteService: Bool { get }
  20. }
  21. extension FetchGlucoseManager {
  22. func updateGlucoseSource(cgmGlucoseSourceType: CGMType, cgmGlucosePluginId: String) {
  23. updateGlucoseSource(cgmGlucoseSourceType: cgmGlucoseSourceType, cgmGlucosePluginId: cgmGlucosePluginId, newManager: nil)
  24. }
  25. }
  26. final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
  27. private let processQueue = DispatchQueue(label: "BaseGlucoseManager.processQueue")
  28. @Injected() var glucoseStorage: GlucoseStorage!
  29. @Injected() var nightscoutManager: NightscoutManager!
  30. @Injected() var tidePoolService: TidePoolManager!
  31. @Injected() var apsManager: APSManager!
  32. @Injected() var settingsManager: SettingsManager!
  33. @Injected() var healthKitManager: HealthKitManager!
  34. @Injected() var deviceDataManager: DeviceDataManager!
  35. @Injected() var pluginCGMManager: PluginManager!
  36. @Injected() var calibrationService: CalibrationService!
  37. private var lifetime = Lifetime()
  38. private let timer = DispatchTimer(timeInterval: 1.minutes.timeInterval)
  39. var cgmGlucoseSourceType: CGMType?
  40. var cgmGlucosePluginId: String?
  41. var cgmManager: CGMManagerUI? {
  42. didSet {
  43. rawCGMManager = cgmManager?.rawValue
  44. }
  45. }
  46. @PersistedProperty(key: "CGMManagerState") var rawCGMManager: CGMManager.RawValue?
  47. private lazy var simulatorSource = GlucoseSimulatorSource()
  48. var shouldSyncToRemoteService: Bool {
  49. guard let cgmManager = cgmManager else {
  50. return true
  51. }
  52. return cgmManager.shouldSyncToRemoteService
  53. }
  54. init(resolver: Resolver) {
  55. injectServices(resolver)
  56. updateGlucoseSource(
  57. cgmGlucoseSourceType: settingsManager.settings.cgm,
  58. cgmGlucosePluginId: settingsManager.settings.cgmPluginIdentifier
  59. )
  60. subscribe()
  61. }
  62. var glucoseSource: GlucoseSource!
  63. func removeCalibrations() {
  64. calibrationService.removeAllCalibrations()
  65. }
  66. func deleteGlucoseSource() {
  67. cgmManager = nil
  68. updateGlucoseSource(
  69. cgmGlucoseSourceType: CGMType.none,
  70. cgmGlucosePluginId: ""
  71. )
  72. }
  73. func updateGlucoseSource(cgmGlucoseSourceType: CGMType, cgmGlucosePluginId: String, newManager: CGMManagerUI?) {
  74. // if changed, remove all calibrations
  75. if self.cgmGlucoseSourceType != cgmGlucoseSourceType || self.cgmGlucosePluginId != cgmGlucosePluginId {
  76. removeCalibrations()
  77. }
  78. self.cgmGlucoseSourceType = cgmGlucoseSourceType
  79. self.cgmGlucosePluginId = cgmGlucosePluginId
  80. // if not plugin, manager is not changed and stay with the "old" value if the user come back to previous cgmtype
  81. // if plugin, if the same pluginID, no change required because the manager is available
  82. // if plugin, if not the same pluginID, need to reset the cgmManager
  83. // if plugin and newManager provides, update cgmManager
  84. debug(.apsManager, "plugin : \(String(describing: cgmManager?.pluginIdentifier))")
  85. if let manager = newManager
  86. {
  87. cgmManager = manager
  88. removeCalibrations()
  89. } else if self.cgmGlucoseSourceType == .plugin, cgmManager == nil, let rawCGMManager = rawCGMManager {
  90. cgmManager = cgmManagerFromRawValue(rawCGMManager)
  91. }
  92. switch self.cgmGlucoseSourceType {
  93. case nil,
  94. .none?:
  95. glucoseSource = nil
  96. case .xdrip:
  97. glucoseSource = AppGroupSource(from: "xDrip", cgmType: .xdrip)
  98. case .nightscout:
  99. glucoseSource = nightscoutManager
  100. case .simulator:
  101. glucoseSource = simulatorSource
  102. case .glucoseDirect:
  103. glucoseSource = AppGroupSource(from: "GlucoseDirect", cgmType: .glucoseDirect)
  104. case .enlite:
  105. glucoseSource = deviceDataManager
  106. case .plugin:
  107. glucoseSource = PluginSource(glucoseStorage: glucoseStorage, glucoseManager: self)
  108. }
  109. // update the config
  110. }
  111. /// Upload cgmManager from raw value
  112. func cgmManagerFromRawValue(_ rawValue: [String: Any]) -> CGMManagerUI? {
  113. guard let rawState = rawValue["state"] as? CGMManager.RawStateValue,
  114. let cgmGlucosePluginId = self.cgmGlucosePluginId,
  115. let Manager = pluginCGMManager.getCGMManagerTypeByIdentifier(cgmGlucosePluginId)
  116. else {
  117. return nil
  118. }
  119. return Manager.init(rawState: rawState)
  120. }
  121. /// function called when a callback is fired by CGM BLE - no more used
  122. public func updateGlucoseStore(newBloodGlucose: [BloodGlucose]) {
  123. let syncDate = glucoseStorage.syncDate()
  124. debug(.deviceManager, "CGM BLE FETCHGLUCOSE : SyncDate is \(syncDate)")
  125. glucoseStoreAndHeartDecision(syncDate: syncDate, glucose: newBloodGlucose)
  126. }
  127. /// function to try to force the refresh of the CGM - generally provide by the pump heartbeat
  128. public func refreshCGM() {
  129. debug(.deviceManager, "refreshCGM by pump")
  130. // updateGlucoseSource(cgmGlucoseSourceType: settingsManager.settings.cgm, cgmGlucosePluginId: settingsManager.settings.cgmPluginIdentifier)
  131. Publishers.CombineLatest3(
  132. Just(glucoseStorage.syncDate()),
  133. healthKitManager.fetch(nil),
  134. glucoseSource.fetchIfNeeded()
  135. )
  136. .eraseToAnyPublisher()
  137. .receive(on: processQueue)
  138. .sink { syncDate, glucoseFromHealth, glucose in
  139. debug(.nightscout, "refreshCGM FETCHGLUCOSE : SyncDate is \(syncDate)")
  140. self.glucoseStoreAndHeartDecision(syncDate: syncDate, glucose: glucose, glucoseFromHealth: glucoseFromHealth)
  141. }
  142. .store(in: &lifetime)
  143. }
  144. private func glucoseStoreAndHeartDecision(syncDate: Date, glucose: [BloodGlucose], glucoseFromHealth: [BloodGlucose] = []) {
  145. // calibration add if required only for sensor
  146. let newGlucose = overcalibrate(entries: glucose)
  147. let allGlucose = newGlucose + glucoseFromHealth
  148. var filteredByDate: [BloodGlucose] = []
  149. var filtered: [BloodGlucose] = []
  150. // start background time extension
  151. var backGroundFetchBGTaskID: UIBackgroundTaskIdentifier?
  152. backGroundFetchBGTaskID = UIApplication.shared.beginBackgroundTask(withName: "save BG starting") {
  153. guard let bg = backGroundFetchBGTaskID else { return }
  154. UIApplication.shared.endBackgroundTask(bg)
  155. backGroundFetchBGTaskID = .invalid
  156. }
  157. guard allGlucose.isNotEmpty else {
  158. if let backgroundTask = backGroundFetchBGTaskID {
  159. UIApplication.shared.endBackgroundTask(backgroundTask)
  160. backGroundFetchBGTaskID = .invalid
  161. }
  162. return
  163. }
  164. filteredByDate = allGlucose.filter { $0.dateString > syncDate }
  165. filtered = glucoseStorage.filterTooFrequentGlucose(filteredByDate, at: syncDate)
  166. guard filtered.isNotEmpty else {
  167. // end of the BG tasks
  168. if let backgroundTask = backGroundFetchBGTaskID {
  169. UIApplication.shared.endBackgroundTask(backgroundTask)
  170. backGroundFetchBGTaskID = .invalid
  171. }
  172. return
  173. }
  174. debug(.deviceManager, "New glucose found")
  175. // filter the data if it is the case
  176. if settingsManager.settings.smoothGlucose {
  177. // limit to 30 minutes of previous BG Data
  178. let oldGlucoses = glucoseStorage.recent().filter {
  179. $0.dateString.addingTimeInterval(31 * 60) > Date()
  180. }
  181. var smoothedValues = oldGlucoses + filtered
  182. // smooth with 3 repeats
  183. for _ in 1 ... 3 {
  184. smoothedValues.smoothSavitzkyGolayQuaDratic(withFilterWidth: 3)
  185. }
  186. // find the new values only
  187. filtered = smoothedValues.filter { $0.dateString > syncDate }
  188. }
  189. glucoseStorage.storeGlucose(filtered)
  190. deviceDataManager.heartbeat(date: Date())
  191. nightscoutManager.uploadGlucose()
  192. tidePoolService.uploadGlucose(device: cgmManager?.cgmManagerStatus.device)
  193. let glucoseForHealth = filteredByDate.filter { !glucoseFromHealth.contains($0) }
  194. guard glucoseForHealth.isNotEmpty else {
  195. // end of the BG tasks
  196. if let backgroundTask = backGroundFetchBGTaskID {
  197. UIApplication.shared.endBackgroundTask(backgroundTask)
  198. backGroundFetchBGTaskID = .invalid
  199. }
  200. return
  201. }
  202. healthKitManager.saveIfNeeded(bloodGlucose: glucoseForHealth)
  203. // end of the BG tasks
  204. if let backgroundTask = backGroundFetchBGTaskID {
  205. UIApplication.shared.endBackgroundTask(backgroundTask)
  206. backGroundFetchBGTaskID = .invalid
  207. }
  208. }
  209. /// The function used to start the timer sync - Function of the variable defined in config
  210. private func subscribe() {
  211. timer.publisher
  212. .receive(on: processQueue)
  213. .flatMap { _ -> AnyPublisher<[BloodGlucose], Never> in
  214. debug(.nightscout, "FetchGlucoseManager timer heartbeat")
  215. // self.updateGlucoseSource(manager: nil)
  216. if let glucoseSource = self.glucoseSource {
  217. return glucoseSource.fetch(self.timer).eraseToAnyPublisher()
  218. } else {
  219. return Empty(completeImmediately: false).eraseToAnyPublisher()
  220. }
  221. }
  222. .sink { glucose in
  223. debug(.nightscout, "FetchGlucoseManager callback sensor")
  224. Publishers.CombineLatest3(
  225. Just(glucose),
  226. Just(self.glucoseStorage.syncDate()),
  227. self.healthKitManager.fetch(nil)
  228. )
  229. .eraseToAnyPublisher()
  230. .sink { newGlucose, syncDate, glucoseFromHealth in
  231. self.glucoseStoreAndHeartDecision(
  232. syncDate: syncDate,
  233. glucose: newGlucose,
  234. glucoseFromHealth: glucoseFromHealth
  235. )
  236. }
  237. .store(in: &self.lifetime)
  238. }
  239. .store(in: &lifetime)
  240. timer.fire()
  241. timer.resume()
  242. }
  243. func sourceInfo() -> [String: Any]? {
  244. glucoseSource.sourceInfo()
  245. }
  246. private func overcalibrate(entries: [BloodGlucose]) -> [BloodGlucose] {
  247. // overcalibrate
  248. var overcalibration: ((Int) -> (Double))?
  249. processQueue.sync { overcalibration = calibrationService.calibrate }
  250. if let overcalibration = overcalibration {
  251. return entries.map { entry in
  252. var entry = entry
  253. entry.glucose = Int(overcalibration(entry.glucose!))
  254. entry.sgv = Int(overcalibration(entry.sgv!))
  255. return entry
  256. }
  257. } else {
  258. return entries
  259. }
  260. }
  261. }
  262. extension CGMManager {
  263. typealias RawValue = [String: Any]
  264. var rawValue: [String: Any] {
  265. [
  266. "managerIdentifier": pluginIdentifier,
  267. "state": rawState
  268. ]
  269. }
  270. }