FetchGlucoseManager.swift 13 KB

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