FetchGlucoseManager.swift 11 KB

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