CGMSettingsStateModel.swift 7.5 KB

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