FetchGlucoseManager.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  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 updateGlucoseSource(cgmGlucoseSourceType: CGMType, cgmGlucosePluginId: String, newManager: CGMManagerUI?)
  11. func deleteGlucoseSource() async
  12. func removeCalibrations()
  13. var glucoseSource: GlucoseSource! { get }
  14. var cgmManager: CGMManagerUI? { get }
  15. var cgmGlucoseSourceType: CGMType { get set }
  16. var cgmGlucosePluginId: String { get }
  17. var settingsManager: SettingsManager! { get }
  18. var shouldSyncToRemoteService: Bool { get }
  19. }
  20. extension FetchGlucoseManager {
  21. func updateGlucoseSource(cgmGlucoseSourceType: CGMType, cgmGlucosePluginId: String) {
  22. updateGlucoseSource(cgmGlucoseSourceType: cgmGlucoseSourceType, cgmGlucosePluginId: cgmGlucosePluginId, newManager: nil)
  23. }
  24. }
  25. final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
  26. private let processQueue = DispatchQueue(label: "BaseGlucoseManager.processQueue")
  27. @Injected() var glucoseStorage: GlucoseStorage!
  28. @Injected() var nightscoutManager: NightscoutManager!
  29. @Injected() var tidepoolService: TidepoolManager!
  30. @Injected() var apsManager: APSManager!
  31. @Injected() var settingsManager: SettingsManager!
  32. @Injected() var healthKitManager: HealthKitManager!
  33. @Injected() var deviceDataManager: DeviceDataManager!
  34. @Injected() var pluginCGMManager: PluginManager!
  35. @Injected() var calibrationService: CalibrationService!
  36. private var lifetime = Lifetime()
  37. private let timer = DispatchTimer(timeInterval: 1.minutes.timeInterval)
  38. var cgmGlucoseSourceType: CGMType = .none
  39. var cgmGlucosePluginId: String = ""
  40. var cgmManager: CGMManagerUI? {
  41. didSet {
  42. rawCGMManager = cgmManager?.rawValue
  43. UserDefaults.standard.clearLegacyCGMManagerRawValue()
  44. }
  45. }
  46. @PersistedProperty(key: "CGMManagerState") var rawCGMManager: CGMManager.RawValue?
  47. private lazy var simulatorSource = GlucoseSimulatorSource()
  48. private let context = CoreDataStack.shared.newTaskContext()
  49. var shouldSyncToRemoteService: Bool {
  50. guard let cgmManager = cgmManager else {
  51. return true
  52. }
  53. return cgmManager.shouldSyncToRemoteService
  54. }
  55. init(resolver: Resolver) {
  56. injectServices(resolver)
  57. // init at the start of the app
  58. cgmGlucoseSourceType = settingsManager.settings.cgm
  59. cgmGlucosePluginId = settingsManager.settings.cgmPluginIdentifier
  60. // load cgmManager
  61. updateGlucoseSource(
  62. cgmGlucoseSourceType: settingsManager.settings.cgm,
  63. cgmGlucosePluginId: settingsManager.settings.cgmPluginIdentifier
  64. )
  65. subscribe()
  66. }
  67. /// The function used to start the timer sync - Function of the variable defined in config
  68. private func subscribe() {
  69. timer.publisher
  70. .receive(on: processQueue)
  71. .flatMap { [self] _ -> AnyPublisher<[BloodGlucose], Never> in
  72. debug(.nightscout, "FetchGlucoseManager timer heartbeat")
  73. if let glucoseSource = self.glucoseSource {
  74. return glucoseSource.fetch(self.timer).eraseToAnyPublisher()
  75. } else {
  76. return Empty(completeImmediately: false).eraseToAnyPublisher()
  77. }
  78. }
  79. .sink { glucose in
  80. debug(.nightscout, "FetchGlucoseManager callback sensor")
  81. Publishers.CombineLatest(
  82. Just(glucose),
  83. Just(self.glucoseStorage.syncDate())
  84. )
  85. .eraseToAnyPublisher()
  86. .sink { newGlucose, syncDate in
  87. Task {
  88. do {
  89. try await self.glucoseStoreAndHeartDecision(
  90. syncDate: syncDate,
  91. glucose: newGlucose
  92. )
  93. } catch {
  94. debug(.deviceManager, "Failed to store glucose: \(error.localizedDescription)")
  95. }
  96. }
  97. }
  98. .store(in: &self.lifetime)
  99. }
  100. .store(in: &lifetime)
  101. timer.fire()
  102. timer.resume()
  103. }
  104. var glucoseSource: GlucoseSource!
  105. func removeCalibrations() {
  106. calibrationService.removeAllCalibrations()
  107. }
  108. @MainActor func deleteGlucoseSource() async {
  109. cgmManager = nil
  110. glucoseSource = nil
  111. settingsManager.settings.cgm = cgmDefaultModel.type
  112. settingsManager.settings.cgmPluginIdentifier = cgmDefaultModel.id
  113. updateGlucoseSource(
  114. cgmGlucoseSourceType: cgmDefaultModel.type,
  115. cgmGlucosePluginId: cgmDefaultModel.id
  116. )
  117. }
  118. func saveConfigManager() {
  119. guard let cgmM = cgmManager else {
  120. return
  121. }
  122. // save the config in rawCGMManager
  123. rawCGMManager = cgmM.rawValue
  124. // sync with upload glucose
  125. settingsManager.settings.uploadGlucose = cgmM.shouldSyncToRemoteService
  126. }
  127. private func updateManagerUnits(_ manager: CGMManagerUI?) {
  128. let units = settingsManager.settings.units
  129. let managerName = cgmManager.map { "\(type(of: $0))" } ?? "nil"
  130. let loopkitUnits: HKUnit = units == .mgdL ? .milligramsPerDeciliter : .millimolesPerLiter
  131. print("manager: \(managerName) is changing units to: \(loopkitUnits.description) ")
  132. manager?.unitDidChange(to: loopkitUnits)
  133. }
  134. func updateGlucoseSource(cgmGlucoseSourceType: CGMType, cgmGlucosePluginId: String, newManager: CGMManagerUI?) {
  135. // if changed, remove all calibrations
  136. if self.cgmGlucoseSourceType != cgmGlucoseSourceType || self.cgmGlucosePluginId != cgmGlucosePluginId {
  137. removeCalibrations()
  138. cgmManager = nil
  139. glucoseSource = nil
  140. }
  141. self.cgmGlucoseSourceType = cgmGlucoseSourceType
  142. self.cgmGlucosePluginId = cgmGlucosePluginId
  143. // if not plugin, manager is not changed and stay with the "old" value if the user come back to previous cgmtype
  144. // if plugin, if the same pluginID, no change required because the manager is available
  145. // if plugin, if not the same pluginID, need to reset the cgmManager
  146. // if plugin and newManager provides, update cgmManager
  147. debug(.apsManager, "plugin : \(String(describing: cgmManager?.pluginIdentifier))")
  148. if let manager = newManager {
  149. cgmManager = manager
  150. removeCalibrations()
  151. // glucoseSource = nil
  152. } else if self.cgmGlucoseSourceType == .plugin, cgmManager == nil, let rawCGMManager = rawCGMManager {
  153. cgmManager = cgmManagerFromRawValue(rawCGMManager)
  154. updateManagerUnits(cgmManager)
  155. } else {
  156. saveConfigManager()
  157. }
  158. if glucoseSource == nil {
  159. switch self.cgmGlucoseSourceType {
  160. case .none:
  161. glucoseSource = nil
  162. case .xdrip:
  163. glucoseSource = AppGroupSource(from: "xDrip", cgmType: .xdrip)
  164. case .nightscout:
  165. glucoseSource = nightscoutManager
  166. case .simulator:
  167. glucoseSource = simulatorSource
  168. case .enlite:
  169. glucoseSource = deviceDataManager
  170. case .plugin:
  171. glucoseSource = PluginSource(glucoseStorage: glucoseStorage, glucoseManager: self)
  172. }
  173. }
  174. }
  175. /// Upload cgmManager from raw value
  176. func cgmManagerFromRawValue(_ rawValue: [String: Any]) -> CGMManagerUI? {
  177. guard let rawState = rawValue["state"] as? CGMManager.RawStateValue,
  178. let Manager = pluginCGMManager.getCGMManagerTypeByIdentifier(cgmGlucosePluginId)
  179. else {
  180. return nil
  181. }
  182. return Manager.init(rawState: rawState)
  183. }
  184. private func fetchGlucose() async throws -> [GlucoseStored]? {
  185. try await CoreDataStack.shared.fetchEntitiesAsync(
  186. ofType: GlucoseStored.self,
  187. onContext: context,
  188. predicate: NSPredicate.predicateFor30MinAgo,
  189. key: "date",
  190. ascending: false,
  191. fetchLimit: 6
  192. ) as? [GlucoseStored]
  193. }
  194. private func processGlucose() async throws -> [BloodGlucose] {
  195. let results = try await fetchGlucose()
  196. return try await context.perform {
  197. guard let results else {
  198. throw CoreDataError.fetchError(function: #function, file: #file)
  199. }
  200. return results.map { result in
  201. BloodGlucose(
  202. sgv: Int(result.glucose),
  203. direction: BloodGlucose.Direction(from: result.direction ?? ""),
  204. date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
  205. dateString: result.date ?? Date(),
  206. unfiltered: Decimal(result.glucose),
  207. filtered: Decimal(result.glucose),
  208. noise: nil,
  209. glucose: Int(result.glucose),
  210. type: "sgv"
  211. )
  212. }
  213. }
  214. }
  215. private func glucoseStoreAndHeartDecision(syncDate: Date, glucose: [BloodGlucose]) async throws {
  216. // calibration add if required only for sensor
  217. let newGlucose = overcalibrate(entries: glucose)
  218. var filteredByDate: [BloodGlucose] = []
  219. var filtered: [BloodGlucose] = []
  220. // Start background task
  221. var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
  222. backgroundTaskID = startBackgroundTask(withName: "Glucose Store and Heartbeat Decision")
  223. guard newGlucose.isNotEmpty else {
  224. endBackgroundTaskSafely(&backgroundTaskID, taskName: "Glucose Store and Heartbeat Decision")
  225. return
  226. }
  227. filteredByDate = newGlucose.filter { $0.dateString > syncDate }
  228. filtered = glucoseStorage.filterTooFrequentGlucose(filteredByDate, at: syncDate)
  229. guard filtered.isNotEmpty else {
  230. endBackgroundTaskSafely(&backgroundTaskID, taskName: "Glucose Store and Heartbeat Decision")
  231. return
  232. }
  233. debug(.deviceManager, "New glucose found")
  234. // filter the data if it is the case
  235. if settingsManager.settings.smoothGlucose {
  236. // limited to 30 min of old glucose data
  237. let oldGlucoseValues = try await processGlucose()
  238. var smoothedValues = oldGlucoseValues + filtered
  239. // smooth with 3 repeats
  240. for _ in 1 ... 3 {
  241. smoothedValues.smoothSavitzkyGolayQuaDratic(withFilterWidth: 3)
  242. }
  243. // find the new values only
  244. filtered = smoothedValues.filter { $0.dateString > syncDate }
  245. }
  246. try await glucoseStorage.storeGlucose(filtered)
  247. deviceDataManager.heartbeat(date: Date())
  248. endBackgroundTaskSafely(&backgroundTaskID, taskName: "Glucose Store and Heartbeat Decision")
  249. }
  250. func sourceInfo() -> [String: Any]? {
  251. glucoseSource.sourceInfo()
  252. }
  253. private func overcalibrate(entries: [BloodGlucose]) -> [BloodGlucose] {
  254. // overcalibrate
  255. var overcalibration: ((Int) -> (Double))?
  256. if let cal = calibrationService {
  257. overcalibration = cal.calibrate
  258. }
  259. if let overcalibration = overcalibration {
  260. return entries.map { entry in
  261. var entry = entry
  262. guard entry.glucose != nil else { return entry }
  263. entry.glucose = Int(overcalibration(entry.glucose!))
  264. entry.sgv = Int(overcalibration(entry.sgv!))
  265. return entry
  266. }
  267. } else {
  268. return entries
  269. }
  270. }
  271. }
  272. extension FetchGlucoseManager {
  273. /// Dispatches given `functionToInvoke` to the CGM manager's queue (if any).
  274. func performOnCGMManagerQueue(_ functionToInvoke: @escaping () -> Void) {
  275. // If a CGM manager exists and it defines a delegate queue, use it
  276. if let cgmManager = self.cgmManager,
  277. let managerQueue = cgmManager.delegateQueue
  278. {
  279. managerQueue.async {
  280. functionToInvoke()
  281. }
  282. } else {
  283. // If there's no cgmManager or no queue, just run the block immediately
  284. // This possibly executes `functionToInvoke` on main thread
  285. functionToInvoke()
  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. }