CGMSettingsStateModel.swift 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. import CGMBLEKit
  2. import Combine
  3. import G7SensorKit
  4. import LoopKitUI
  5. import SwiftUI
  6. /// For a full description of the events that can happen for the CGM lifecycle, see comment at the top
  7. /// of HomeStateModel+CGM since these are the same events
  8. struct CGMModel: Identifiable, Hashable {
  9. var id: String
  10. var type: CGMType
  11. var displayName: String
  12. var subtitle: String
  13. }
  14. struct CGMOption {
  15. let name: String
  16. let predicate: (CGMModel) -> Bool
  17. }
  18. let cgmDefaultModel = CGMModel(
  19. id: CGMType.none.id,
  20. type: .none,
  21. displayName: CGMType.none.displayName,
  22. subtitle: CGMType.none.subtitle
  23. )
  24. extension CGMSettings {
  25. final class StateModel: BaseStateModel<Provider> {
  26. // Singleton implementation
  27. private static var _shared: StateModel?
  28. static var shared: StateModel {
  29. if _shared == nil {
  30. _shared = StateModel()
  31. _shared?.resolver = TrioApp().resolver
  32. }
  33. return _shared!
  34. }
  35. @Injected() var fetchGlucoseManager: FetchGlucoseManager!
  36. @Injected() var pluginCGMManager: PluginManager!
  37. @Injected() var broadcaster: Broadcaster!
  38. @Injected() var nightscoutManager: NightscoutManager!
  39. @Published var units: GlucoseUnits = .mgdL
  40. @Published var shouldDisplayCGMSetupSheet: Bool = false
  41. @Published var cgmCurrent = cgmDefaultModel
  42. @Published var smoothGlucose = false
  43. @Published var cgmTransmitterDeviceAddress: String? = nil
  44. @Published var listOfCGM: [CGMModel] = []
  45. @Published var url: URL?
  46. var shouldRunDeleteOnSettingsChange = true
  47. override func subscribe() {
  48. units = settingsManager.settings.units
  49. broadcaster.register(SettingsObserver.self, observer: self)
  50. // collect the list of CGM available with plugins and CGMType defined manually
  51. listOfCGM = (
  52. CGMType.allCases.filter { $0 != CGMType.plugin }.map {
  53. CGMModel(id: $0.id, type: $0, displayName: $0.displayName, subtitle: $0.subtitle)
  54. } +
  55. pluginCGMManager.availableCGMManagers.map {
  56. CGMModel(
  57. id: $0.identifier,
  58. type: CGMType.plugin,
  59. displayName: $0.localizedTitle,
  60. subtitle: $0.localizedTitle
  61. )
  62. }
  63. ).sorted(by: { lhs, rhs in
  64. if lhs.displayName == "None" {
  65. return true
  66. } else if rhs.displayName == "None" {
  67. return false
  68. } else {
  69. return lhs.displayName < rhs.displayName
  70. }
  71. })
  72. switch settingsManager.settings.cgm {
  73. case .plugin:
  74. if let cgmPluginInfo = listOfCGM.first(where: { $0.id == settingsManager.settings.cgmPluginIdentifier }) {
  75. cgmCurrent = CGMModel(
  76. id: settingsManager.settings.cgmPluginIdentifier,
  77. type: .plugin,
  78. displayName: cgmPluginInfo.displayName,
  79. subtitle: cgmPluginInfo.subtitle
  80. )
  81. } else {
  82. // no more type of plugin available - fallback to default model
  83. cgmCurrent = cgmDefaultModel
  84. }
  85. default:
  86. cgmCurrent = CGMModel(
  87. id: settingsManager.settings.cgm.id,
  88. type: settingsManager.settings.cgm,
  89. displayName: settingsManager.settings.cgm.displayName,
  90. subtitle: settingsManager.settings.cgm.subtitle
  91. )
  92. }
  93. url = nightscoutManager.cgmURL
  94. switch url?.absoluteString {
  95. case "http://127.0.0.1:1979":
  96. url = URL(string: "spikeapp://")!
  97. case "http://127.0.0.1:17580":
  98. url = URL(string: "diabox://")!
  99. default: break
  100. }
  101. cgmTransmitterDeviceAddress = UserDefaults.standard.cgmTransmitterDeviceAddress
  102. subscribeSetting(\.smoothGlucose, on: $smoothGlucose, initial: { smoothGlucose = $0 })
  103. }
  104. // this function will get called for all CGM types (plugin and non plugin)
  105. func addCGM(cgm: CGMModel) {
  106. cgmCurrent = cgm
  107. switch cgm.type {
  108. case .plugin:
  109. shouldDisplayCGMSetupSheet.toggle()
  110. default:
  111. // non plugin CGM types should be considered onboarded right away
  112. shouldDisplayCGMSetupSheet = true
  113. settingsManager.settings.cgm = cgmCurrent.type
  114. settingsManager.settings.cgmPluginIdentifier = ""
  115. fetchGlucoseManager.updateGlucoseSource(cgmGlucoseSourceType: cgmCurrent.type, cgmGlucosePluginId: cgmCurrent.id)
  116. broadcaster.notify(GlucoseObserver.self, on: .main) {
  117. $0.glucoseDidUpdate([])
  118. }
  119. }
  120. }
  121. // Note: This function does _not_ get called for plugin CGMs
  122. // instead, they will get cgmManagerWantsDeletion events which
  123. // are handled by PluginSource
  124. func deleteCGM() {
  125. Task {
  126. await self.fetchGlucoseManager?.deleteGlucoseSource()
  127. await MainActor.run {
  128. self.shouldDisplayCGMSetupSheet = false
  129. broadcaster.notify(GlucoseObserver.self, on: .main) {
  130. $0.glucoseDidUpdate([])
  131. }
  132. }
  133. }
  134. }
  135. }
  136. }
  137. extension CGMSettings.StateModel: CompletionDelegate {
  138. func completionNotifyingDidComplete(_: CompletionNotifying) {
  139. Task {
  140. // this sleep is because this event and cgmManagerWantsDeletion
  141. // are called in parallel.
  142. try await Task.sleep(for: .seconds(0.2))
  143. await MainActor.run {
  144. if fetchGlucoseManager.cgmGlucoseSourceType == .none {
  145. cgmCurrent = cgmDefaultModel
  146. }
  147. }
  148. }
  149. shouldDisplayCGMSetupSheet = false
  150. }
  151. }
  152. extension CGMSettings.StateModel: CGMManagerOnboardingDelegate {
  153. func cgmManagerOnboarding(didCreateCGMManager manager: LoopKitUI.CGMManagerUI) {
  154. // cgmCurrent should have been set in addCGM
  155. debug(.service, "didCreateCGMManager called \(cgmCurrent)")
  156. settingsManager.settings.cgm = cgmCurrent.type
  157. settingsManager.settings.cgmPluginIdentifier = cgmCurrent.id
  158. fetchGlucoseManager.updateGlucoseSource(
  159. cgmGlucoseSourceType: cgmCurrent.type,
  160. cgmGlucosePluginId: cgmCurrent.id,
  161. newManager: manager
  162. )
  163. DispatchQueue.main.async {
  164. self.broadcaster.notify(GlucoseObserver.self, on: .main) {
  165. $0.glucoseDidUpdate([])
  166. }
  167. }
  168. }
  169. func cgmManagerOnboarding(didOnboardCGMManager _: LoopKitUI.CGMManagerUI) {
  170. // nothing to do ?
  171. }
  172. }
  173. extension CGMSettings.StateModel: SettingsObserver {
  174. func settingsDidChange(_: TrioSettings) {
  175. units = settingsManager.settings.units
  176. // Deletes are handled differently for plugins vs non plugins
  177. // but both will call deleteGlucoseSource on the fetchGlucoseManager
  178. // so we listen for changes to the cgm setting and update our internal
  179. // state accordingly
  180. if settingsManager.settings.cgm == .none, shouldRunDeleteOnSettingsChange {
  181. shouldRunDeleteOnSettingsChange = false
  182. cgmCurrent = cgmDefaultModel
  183. DispatchQueue.main.async {
  184. self.broadcaster.notify(GlucoseObserver.self, on: .main) {
  185. $0.glucoseDidUpdate([])
  186. }
  187. }
  188. } else {
  189. shouldRunDeleteOnSettingsChange = true
  190. }
  191. }
  192. }