FetchGlucoseManager.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  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. private let context = CoreDataStack.shared.newTaskContext()
  51. var shouldSyncToRemoteService: Bool {
  52. guard let cgmManager = cgmManager else {
  53. return true
  54. }
  55. return cgmManager.shouldSyncToRemoteService
  56. }
  57. init(resolver: Resolver) {
  58. injectServices(resolver)
  59. // init at the start of the app
  60. cgmGlucoseSourceType = settingsManager.settings.cgm
  61. cgmGlucosePluginId = settingsManager.settings.cgmPluginIdentifier
  62. // load cgmManager
  63. updateGlucoseSource(
  64. cgmGlucoseSourceType: settingsManager.settings.cgm,
  65. cgmGlucosePluginId: settingsManager.settings.cgmPluginIdentifier
  66. )
  67. subscribe()
  68. }
  69. var glucoseSource: GlucoseSource!
  70. func removeCalibrations() {
  71. calibrationService.removeAllCalibrations()
  72. }
  73. func deleteGlucoseSource() {
  74. cgmManager = nil
  75. updateGlucoseSource(
  76. cgmGlucoseSourceType: CGMType.none,
  77. cgmGlucosePluginId: ""
  78. )
  79. }
  80. func saveConfigManager() {
  81. guard let cgmM = cgmManager else {
  82. return
  83. }
  84. // save the config in rawCGMManager
  85. rawCGMManager = cgmM.rawValue
  86. // sync with upload glucose
  87. settingsManager.settings.uploadGlucose = cgmM.shouldSyncToRemoteService
  88. }
  89. private func updateManagerUnits(_ manager: CGMManagerUI?) {
  90. let units = settingsManager.settings.units
  91. let managerName = cgmManager.map { "\(type(of: $0))" } ?? "nil"
  92. let loopkitUnits: HKUnit = units == .mgdL ? .milligramsPerDeciliter : .millimolesPerLiter
  93. print("manager: \(managerName) is changing units to: \(loopkitUnits.description) ")
  94. manager?.unitDidChange(to: loopkitUnits)
  95. }
  96. func updateGlucoseSource(cgmGlucoseSourceType: CGMType, cgmGlucosePluginId: String, newManager: CGMManagerUI?) {
  97. // if changed, remove all calibrations
  98. if self.cgmGlucoseSourceType != cgmGlucoseSourceType || self.cgmGlucosePluginId != cgmGlucosePluginId {
  99. removeCalibrations()
  100. cgmManager = nil
  101. glucoseSource = nil
  102. }
  103. self.cgmGlucoseSourceType = cgmGlucoseSourceType
  104. self.cgmGlucosePluginId = cgmGlucosePluginId
  105. // if not plugin, manager is not changed and stay with the "old" value if the user come back to previous cgmtype
  106. // if plugin, if the same pluginID, no change required because the manager is available
  107. // if plugin, if not the same pluginID, need to reset the cgmManager
  108. // if plugin and newManager provides, update cgmManager
  109. debug(.apsManager, "plugin : \(String(describing: cgmManager?.pluginIdentifier))")
  110. if let manager = newManager
  111. {
  112. cgmManager = manager
  113. removeCalibrations()
  114. } else if self.cgmGlucoseSourceType == .plugin, cgmManager == nil, let rawCGMManager = rawCGMManager {
  115. cgmManager = cgmManagerFromRawValue(rawCGMManager)
  116. updateManagerUnits(cgmManager)
  117. } else {
  118. saveConfigManager()
  119. }
  120. if glucoseSource == nil {
  121. switch self.cgmGlucoseSourceType {
  122. case .none:
  123. glucoseSource = nil
  124. case .xdrip:
  125. glucoseSource = AppGroupSource(from: "xDrip", cgmType: .xdrip)
  126. case .nightscout:
  127. glucoseSource = nightscoutManager
  128. case .simulator:
  129. glucoseSource = simulatorSource
  130. case .enlite:
  131. glucoseSource = deviceDataManager
  132. case .plugin:
  133. glucoseSource = PluginSource(glucoseStorage: glucoseStorage, glucoseManager: self)
  134. }
  135. }
  136. }
  137. /// Upload cgmManager from raw value
  138. func cgmManagerFromRawValue(_ rawValue: [String: Any]) -> CGMManagerUI? {
  139. guard let rawState = rawValue["state"] as? CGMManager.RawStateValue,
  140. let Manager = pluginCGMManager.getCGMManagerTypeByIdentifier(cgmGlucosePluginId)
  141. else {
  142. return nil
  143. }
  144. return Manager.init(rawState: rawState)
  145. }
  146. /// function called when a callback is fired by CGM BLE - no more used
  147. public func updateGlucoseStore(newBloodGlucose: [BloodGlucose]) {
  148. let syncDate = glucoseStorage.syncDate()
  149. debug(.deviceManager, "CGM BLE FETCHGLUCOSE : SyncDate is \(syncDate)")
  150. glucoseStoreAndHeartDecision(syncDate: syncDate, glucose: newBloodGlucose)
  151. }
  152. /// function to try to force the refresh of the CGM - generally provide by the pump heartbeat
  153. public func refreshCGM() {
  154. debug(.deviceManager, "refreshCGM by pump")
  155. Publishers.CombineLatest(
  156. Just(glucoseStorage.syncDate()),
  157. glucoseSource.fetchIfNeeded()
  158. )
  159. .eraseToAnyPublisher()
  160. .receive(on: processQueue)
  161. .sink { syncDate, glucose in
  162. debug(.nightscout, "refreshCGM FETCHGLUCOSE : SyncDate is \(syncDate)")
  163. self.glucoseStoreAndHeartDecision(syncDate: syncDate, glucose: glucose)
  164. }
  165. .store(in: &lifetime)
  166. }
  167. private func fetchGlucose() -> [GlucoseStored]? {
  168. CoreDataStack.shared.fetchEntities(
  169. ofType: GlucoseStored.self,
  170. onContext: context,
  171. predicate: NSPredicate.predicateFor30MinAgo,
  172. key: "date",
  173. ascending: false,
  174. fetchLimit: 6
  175. ) as? [GlucoseStored]
  176. }
  177. private func processGlucose() -> [BloodGlucose] {
  178. context.performAndWait {
  179. guard let results = fetchGlucose() else { return [] }
  180. return results.map { result in
  181. BloodGlucose(
  182. sgv: Int(result.glucose),
  183. direction: BloodGlucose.Direction(from: result.direction ?? ""),
  184. date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
  185. dateString: result.date ?? Date(),
  186. unfiltered: Decimal(result.glucose),
  187. filtered: Decimal(result.glucose),
  188. noise: nil,
  189. glucose: Int(result.glucose)
  190. )
  191. }
  192. }
  193. }
  194. private func glucoseStoreAndHeartDecision(syncDate: Date, glucose: [BloodGlucose]) {
  195. // calibration add if required only for sensor
  196. let newGlucose = overcalibrate(entries: glucose)
  197. var filteredByDate: [BloodGlucose] = []
  198. var filtered: [BloodGlucose] = []
  199. // start background time extension
  200. var backGroundFetchBGTaskID: UIBackgroundTaskIdentifier?
  201. backGroundFetchBGTaskID = UIApplication.shared.beginBackgroundTask(withName: "save BG starting") {
  202. guard let bg = backGroundFetchBGTaskID else { return }
  203. UIApplication.shared.endBackgroundTask(bg)
  204. backGroundFetchBGTaskID = .invalid
  205. }
  206. guard newGlucose.isNotEmpty else {
  207. if let backgroundTask = backGroundFetchBGTaskID {
  208. UIApplication.shared.endBackgroundTask(backgroundTask)
  209. backGroundFetchBGTaskID = .invalid
  210. }
  211. return
  212. }
  213. filteredByDate = newGlucose.filter { $0.dateString > syncDate }
  214. filtered = glucoseStorage.filterTooFrequentGlucose(filteredByDate, at: syncDate)
  215. guard filtered.isNotEmpty else {
  216. // end of the Background tasks
  217. if let backgroundTask = backGroundFetchBGTaskID {
  218. UIApplication.shared.endBackgroundTask(backgroundTask)
  219. backGroundFetchBGTaskID = .invalid
  220. }
  221. return
  222. }
  223. debug(.deviceManager, "New glucose found")
  224. // filter the data if it is the case
  225. if settingsManager.settings.smoothGlucose {
  226. // limited to 30 min of old glucose data
  227. let oldGlucoseValues = processGlucose()
  228. var smoothedValues = oldGlucoseValues + filtered
  229. // smooth with 3 repeats
  230. for _ in 1 ... 3 {
  231. smoothedValues.smoothSavitzkyGolayQuaDratic(withFilterWidth: 3)
  232. }
  233. // find the new values only
  234. filtered = smoothedValues.filter { $0.dateString > syncDate }
  235. }
  236. glucoseStorage.storeGlucose(filtered)
  237. deviceDataManager.heartbeat(date: Date())
  238. // End of the Background tasks
  239. if let backgroundTask = backGroundFetchBGTaskID {
  240. UIApplication.shared.endBackgroundTask(backgroundTask)
  241. backGroundFetchBGTaskID = .invalid
  242. }
  243. }
  244. /// The function used to start the timer sync - Function of the variable defined in config
  245. private func subscribe() {
  246. timer.publisher
  247. .receive(on: processQueue)
  248. .flatMap { [self] _ -> AnyPublisher<[BloodGlucose], Never> in
  249. debug(.nightscout, "FetchGlucoseManager timer heartbeat")
  250. if let glucoseSource = self.glucoseSource {
  251. return glucoseSource.fetch(self.timer).eraseToAnyPublisher()
  252. } else {
  253. return Empty(completeImmediately: false).eraseToAnyPublisher()
  254. }
  255. }
  256. .sink { glucose in
  257. debug(.nightscout, "FetchGlucoseManager callback sensor")
  258. Publishers.CombineLatest(
  259. Just(glucose),
  260. Just(self.glucoseStorage.syncDate())
  261. )
  262. .eraseToAnyPublisher()
  263. .sink { newGlucose, syncDate in
  264. self.glucoseStoreAndHeartDecision(
  265. syncDate: syncDate,
  266. glucose: newGlucose
  267. )
  268. }
  269. .store(in: &self.lifetime)
  270. }
  271. .store(in: &lifetime)
  272. timer.fire()
  273. timer.resume()
  274. }
  275. func sourceInfo() -> [String: Any]? {
  276. glucoseSource.sourceInfo()
  277. }
  278. private func overcalibrate(entries: [BloodGlucose]) -> [BloodGlucose] {
  279. // overcalibrate
  280. var overcalibration: ((Int) -> (Double))?
  281. if let cal = calibrationService {
  282. overcalibration = cal.calibrate
  283. }
  284. if let overcalibration = overcalibration {
  285. return entries.map { entry in
  286. var entry = entry
  287. guard entry.glucose != nil else { return entry }
  288. entry.glucose = Int(overcalibration(entry.glucose!))
  289. entry.sgv = Int(overcalibration(entry.sgv!))
  290. return entry
  291. }
  292. } else {
  293. return entries
  294. }
  295. }
  296. }
  297. extension CGMManager {
  298. typealias RawValue = [String: Any]
  299. var rawValue: [String: Any] {
  300. [
  301. "managerIdentifier": pluginIdentifier,
  302. "state": rawState
  303. ]
  304. }
  305. }