CGMStateModel.swift 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. import CGMBLEKit
  2. import Combine
  3. import G7SensorKit
  4. import LoopKitUI
  5. import SwiftUI
  6. struct cgmName: Identifiable, Hashable {
  7. var id: String
  8. var type: CGMType
  9. var displayName: String
  10. var subtitle: String
  11. }
  12. let cgmDefaultName = cgmName(
  13. id: CGMType.none.id,
  14. type: .none,
  15. displayName: CGMType.none.displayName,
  16. subtitle: CGMType.none.subtitle
  17. )
  18. extension CGM {
  19. final class StateModel: BaseStateModel<Provider> {
  20. @Injected() var cgmManager: FetchGlucoseManager!
  21. @Injected() var calendarManager: CalendarManager!
  22. @Injected() var pluginCGMManager: PluginManager!
  23. @Injected() private var broadcaster: Broadcaster!
  24. @Injected() var nightscoutManager: NightscoutManager!
  25. @Published var setupCGM: Bool = false
  26. @Published var cgmCurrent = cgmDefaultName
  27. @Published var smoothGlucose = false
  28. @Published var createCalendarEvents = false
  29. @Published var displayCalendarIOBandCOB = false
  30. @Published var displayCalendarEmojis = false
  31. @Published var calendarIDs: [String] = []
  32. @Published var currentCalendarID: String = ""
  33. @Persisted(key: "CalendarManager.currentCalendarID") var storedCalendarID: String? = nil
  34. @Published var cgmTransmitterDeviceAddress: String? = nil
  35. @Published var listOfCGM: [cgmName] = []
  36. @Published var url: URL?
  37. override func subscribe() {
  38. // collect the list of CGM available with plugins and CGMType defined manually
  39. listOfCGM = CGMType.allCases.filter { $0 != CGMType.plugin }.map {
  40. cgmName(id: $0.id, type: $0, displayName: $0.displayName, subtitle: $0.subtitle)
  41. } +
  42. pluginCGMManager.availableCGMManagers.map {
  43. cgmName(id: $0.identifier, type: CGMType.plugin, displayName: $0.localizedTitle, subtitle: $0.localizedTitle)
  44. }
  45. switch settingsManager.settings.cgm {
  46. case .plugin:
  47. if let cgmPluginInfo = listOfCGM.first(where: { $0.id == settingsManager.settings.cgmPluginIdentifier }) {
  48. cgmCurrent = cgmName(
  49. id: settingsManager.settings.cgmPluginIdentifier,
  50. type: .plugin,
  51. displayName: cgmPluginInfo.displayName,
  52. subtitle: cgmPluginInfo.subtitle
  53. )
  54. } else {
  55. // no more type of plugin available - restart to defaut
  56. cgmCurrent = cgmDefaultName
  57. }
  58. default:
  59. cgmCurrent = cgmName(
  60. id: settingsManager.settings.cgm.id,
  61. type: settingsManager.settings.cgm,
  62. displayName: settingsManager.settings.cgm.displayName,
  63. subtitle: settingsManager.settings.cgm.subtitle
  64. )
  65. }
  66. url = nightscoutManager.cgmURL
  67. switch url?.absoluteString {
  68. case "http://127.0.0.1:1979":
  69. url = URL(string: "spikeapp://")!
  70. case "http://127.0.0.1:17580":
  71. url = URL(string: "diabox://")!
  72. // case CGMType.libreTransmitter.appURL?.absoluteString:
  73. // showModal(for: .libreConfig)
  74. default: break
  75. }
  76. currentCalendarID = storedCalendarID ?? ""
  77. calendarIDs = calendarManager.calendarIDs()
  78. cgmTransmitterDeviceAddress = UserDefaults.standard.cgmTransmitterDeviceAddress
  79. subscribeSetting(\.useCalendar, on: $createCalendarEvents) { createCalendarEvents = $0 }
  80. subscribeSetting(\.displayCalendarIOBandCOB, on: $displayCalendarIOBandCOB) { displayCalendarIOBandCOB = $0 }
  81. subscribeSetting(\.displayCalendarEmojis, on: $displayCalendarEmojis) { displayCalendarEmojis = $0 }
  82. subscribeSetting(\.smoothGlucose, on: $smoothGlucose, initial: { smoothGlucose = $0 })
  83. $cgmCurrent
  84. .removeDuplicates()
  85. .sink { [weak self] value in
  86. guard let self = self else { return }
  87. guard self.cgmManager.cgmGlucoseSourceType != nil else {
  88. self.settingsManager.settings.cgm = .none
  89. return
  90. }
  91. if value.type != self.settingsManager.settings.cgm ||
  92. value.id != self.settingsManager.settings.cgmPluginIdentifier
  93. {
  94. self.settingsManager.settings.cgm = value.type
  95. self.settingsManager.settings.cgmPluginIdentifier = value.id
  96. self.cgmManager.updateGlucoseSource(
  97. cgmGlucoseSourceType: value.type,
  98. cgmGlucosePluginId: value.id
  99. )
  100. self.setupCGM = false
  101. }
  102. }
  103. .store(in: &lifetime)
  104. $createCalendarEvents
  105. .removeDuplicates()
  106. .flatMap { [weak self] ok -> AnyPublisher<Bool, Never> in
  107. guard ok, let self = self else { return Just(false).eraseToAnyPublisher() }
  108. return self.calendarManager.requestAccessIfNeeded()
  109. }
  110. .map { [weak self] ok -> [String] in
  111. guard ok, let self = self else { return [] }
  112. return self.calendarManager.calendarIDs()
  113. }
  114. .receive(on: DispatchQueue.main)
  115. .weakAssign(to: \.calendarIDs, on: self)
  116. .store(in: &lifetime)
  117. $currentCalendarID
  118. .removeDuplicates()
  119. .sink { [weak self] id in
  120. guard id.isNotEmpty else {
  121. self?.calendarManager.currentCalendarID = nil
  122. return
  123. }
  124. self?.calendarManager.currentCalendarID = id
  125. }
  126. .store(in: &lifetime)
  127. }
  128. func displayNameOfApp() -> String? {
  129. guard cgmManager != nil else { return nil }
  130. var nameOfApp = "Open Application"
  131. switch cgmManager.cgmGlucoseSourceType {
  132. case .plugin:
  133. nameOfApp = "Open " + (cgmManager.cgmManager?.localizedTitle ?? "Application")
  134. default:
  135. nameOfApp = "Open " + cgmManager.cgmGlucoseSourceType.displayName
  136. }
  137. return nameOfApp
  138. }
  139. func urlOfApp() -> URL? {
  140. guard cgmManager != nil else { return nil }
  141. switch cgmManager.cgmGlucoseSourceType {
  142. case .plugin:
  143. return cgmManager.cgmManager?.appURL
  144. default:
  145. return cgmManager.cgmGlucoseSourceType.appURL
  146. }
  147. }
  148. }
  149. }
  150. extension CGM.StateModel: CompletionDelegate {
  151. func completionNotifyingDidComplete(_: CompletionNotifying) {
  152. setupCGM = false
  153. // if CGM was deleted
  154. if cgmManager.cgmGlucoseSourceType == nil {
  155. cgmCurrent = cgmDefaultName
  156. settingsManager.settings.cgm = cgmDefaultName.type
  157. settingsManager.settings.cgmPluginIdentifier = cgmDefaultName.id
  158. cgmManager.deleteGlucoseSource()
  159. } else {
  160. cgmManager.updateGlucoseSource(cgmGlucoseSourceType: cgmCurrent.type, cgmGlucosePluginId: cgmCurrent.id)
  161. }
  162. // update if required the Glucose source
  163. DispatchQueue.main.async {
  164. self.broadcaster.notify(GlucoseObserver.self, on: .main) {
  165. $0.glucoseDidUpdate([])
  166. }
  167. }
  168. }
  169. }
  170. extension CGM.StateModel: CGMManagerOnboardingDelegate {
  171. func cgmManagerOnboarding(didCreateCGMManager manager: LoopKitUI.CGMManagerUI) {
  172. // update the glucose source
  173. cgmManager.updateGlucoseSource(
  174. cgmGlucoseSourceType: cgmCurrent.type,
  175. cgmGlucosePluginId: cgmCurrent.id,
  176. newManager: manager
  177. )
  178. }
  179. func cgmManagerOnboarding(didOnboardCGMManager _: LoopKitUI.CGMManagerUI) {
  180. // nothing to do ?
  181. }
  182. }