CGMSettingsStateModel.swift 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  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. struct OtherCGMSourceCompletionNotifying: CompletionNotifying {
  23. var completionDelegate: (any LoopKitUI.CompletionDelegate)?
  24. }
  25. class CGMSetupCompletionNotifying: CompletionNotifying {
  26. var completionDelegate: (any LoopKitUI.CompletionDelegate)?
  27. }
  28. class CGMDeletionCompletionNotifying: CompletionNotifying {
  29. var completionDelegate: (any LoopKitUI.CompletionDelegate)?
  30. }
  31. extension CGMSettings {
  32. final class StateModel: BaseStateModel<Provider> {
  33. // Singleton implementation
  34. private static var _shared: StateModel?
  35. static var shared: StateModel {
  36. if _shared == nil {
  37. _shared = StateModel()
  38. _shared?.resolver = TrioApp().resolver
  39. }
  40. return _shared!
  41. }
  42. @Injected() var fetchGlucoseManager: FetchGlucoseManager!
  43. @Injected() var pluginCGMManager: PluginManager!
  44. @Injected() private var broadcaster: Broadcaster!
  45. @Injected() var nightscoutManager: NightscoutManager!
  46. @Published var units: GlucoseUnits = .mgdL
  47. @Published var shouldDisplayCGMSetupSheet: Bool = false
  48. @Published var cgmCurrent = cgmDefaultModel
  49. @Published var smoothGlucose = false
  50. @Published var cgmTransmitterDeviceAddress: String? = nil
  51. @Published var listOfCGM: [CGMModel] = []
  52. @Published var url: URL?
  53. override func subscribe() {
  54. units = settingsManager.settings.units
  55. // collect the list of CGM available with plugins and CGMType defined manually
  56. listOfCGM = (
  57. CGMType.allCases.filter { $0 != CGMType.plugin }.map {
  58. CGMModel(id: $0.id, type: $0, displayName: $0.displayName, subtitle: $0.subtitle)
  59. } +
  60. pluginCGMManager.availableCGMManagers.map {
  61. CGMModel(
  62. id: $0.identifier,
  63. type: CGMType.plugin,
  64. displayName: $0.localizedTitle,
  65. subtitle: $0.localizedTitle
  66. )
  67. }
  68. ).sorted(by: { lhs, rhs in
  69. if lhs.displayName == "None" {
  70. return true
  71. } else if rhs.displayName == "None" {
  72. return false
  73. } else {
  74. return lhs.displayName < rhs.displayName
  75. }
  76. })
  77. switch settingsManager.settings.cgm {
  78. case .plugin:
  79. if let cgmPluginInfo = listOfCGM.first(where: { $0.id == settingsManager.settings.cgmPluginIdentifier }) {
  80. cgmCurrent = CGMModel(
  81. id: settingsManager.settings.cgmPluginIdentifier,
  82. type: .plugin,
  83. displayName: cgmPluginInfo.displayName,
  84. subtitle: cgmPluginInfo.subtitle
  85. )
  86. } else {
  87. // no more type of plugin available - fallback to default model
  88. cgmCurrent = cgmDefaultModel
  89. }
  90. default:
  91. cgmCurrent = CGMModel(
  92. id: settingsManager.settings.cgm.id,
  93. type: settingsManager.settings.cgm,
  94. displayName: settingsManager.settings.cgm.displayName,
  95. subtitle: settingsManager.settings.cgm.subtitle
  96. )
  97. }
  98. url = nightscoutManager.cgmURL
  99. switch url?.absoluteString {
  100. case "http://127.0.0.1:1979":
  101. url = URL(string: "spikeapp://")!
  102. case "http://127.0.0.1:17580":
  103. url = URL(string: "diabox://")!
  104. default: break
  105. }
  106. cgmTransmitterDeviceAddress = UserDefaults.standard.cgmTransmitterDeviceAddress
  107. subscribeSetting(\.smoothGlucose, on: $smoothGlucose, initial: { smoothGlucose = $0 })
  108. }
  109. func addCGM(cgm: CGMModel) {
  110. cgmCurrent = cgm
  111. switch cgmCurrent.type {
  112. case .plugin:
  113. shouldDisplayCGMSetupSheet.toggle()
  114. default:
  115. fetchGlucoseManager.cgmGlucoseSourceType = cgmCurrent.type
  116. completionNotifyingDidComplete(OtherCGMSourceCompletionNotifying())
  117. }
  118. }
  119. func deleteCGM() {
  120. fetchGlucoseManager.performOnCGMManagerQueue {
  121. // Call plugin functionality on the manager queue (or at least attempt to)
  122. Task {
  123. await self.fetchGlucoseManager?.deleteGlucoseSource()
  124. }
  125. // UI updates go back to Main
  126. DispatchQueue.main.async {
  127. self.shouldDisplayCGMSetupSheet = false
  128. self.completionNotifyingDidComplete(CGMDeletionCompletionNotifying())
  129. }
  130. }
  131. }
  132. }
  133. }
  134. extension CGMSettings.StateModel: CompletionDelegate {
  135. func completionNotifyingDidComplete(_: CompletionNotifying) {
  136. // if CGM was deleted
  137. if fetchGlucoseManager.cgmGlucoseSourceType == .none {
  138. cgmCurrent = cgmDefaultModel
  139. settingsManager.settings.cgm = cgmDefaultModel.type
  140. settingsManager.settings.cgmPluginIdentifier = cgmDefaultModel.id
  141. Task {
  142. await fetchGlucoseManager.deleteGlucoseSource()
  143. }
  144. shouldDisplayCGMSetupSheet = false
  145. } else {
  146. settingsManager.settings.cgm = cgmCurrent.type
  147. settingsManager.settings.cgmPluginIdentifier = cgmCurrent.id
  148. fetchGlucoseManager.updateGlucoseSource(cgmGlucoseSourceType: cgmCurrent.type, cgmGlucosePluginId: cgmCurrent.id)
  149. shouldDisplayCGMSetupSheet = cgmCurrent.type == .simulator || cgmCurrent.type == .nightscout || cgmCurrent
  150. .type == .xdrip || cgmCurrent.type == .enlite
  151. }
  152. // update glucose source if required
  153. DispatchQueue.main.async {
  154. self.broadcaster.notify(GlucoseObserver.self, on: .main) {
  155. $0.glucoseDidUpdate([])
  156. }
  157. }
  158. }
  159. }
  160. extension CGMSettings.StateModel: CGMManagerOnboardingDelegate {
  161. func cgmManagerOnboarding(didCreateCGMManager manager: LoopKitUI.CGMManagerUI) {
  162. // update the glucose source
  163. fetchGlucoseManager.updateGlucoseSource(
  164. cgmGlucoseSourceType: cgmCurrent.type,
  165. cgmGlucosePluginId: cgmCurrent.id,
  166. newManager: manager
  167. )
  168. }
  169. func cgmManagerOnboarding(didOnboardCGMManager _: LoopKitUI.CGMManagerUI) {
  170. // nothing to do ?
  171. }
  172. }
  173. extension CGMSettings.StateModel {
  174. func settingsDidChange(_: TrioSettings) {
  175. units = settingsManager.settings.units
  176. }
  177. }