NightscoutConfigStateModel.swift 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. import Combine
  2. import CoreData
  3. import G7SensorKit
  4. import LoopKit
  5. import SwiftDate
  6. import SwiftUI
  7. extension NightscoutConfig {
  8. final class StateModel: BaseStateModel<Provider> {
  9. @Injected() private var keychain: Keychain!
  10. @Injected() private var nightscoutManager: NightscoutManager!
  11. @Injected() private var glucoseStorage: GlucoseStorage!
  12. @Injected() private var healthKitManager: HealthKitManager!
  13. @Injected() private var cgmManager: FetchGlucoseManager!
  14. @Injected() private var storage: FileStorage!
  15. @Injected() var apsManager: APSManager!
  16. @Published var url = ""
  17. @Published var secret = ""
  18. @Published var message = ""
  19. @Published var isValidURL: Bool = false
  20. @Published var connecting = false
  21. @Published var backfilling = false
  22. @Published var isUploadEnabled = false // Allow uploads
  23. @Published var isDownloadEnabled = false // Allow downloads
  24. @Published var uploadGlucose = true // Upload Glucose
  25. @Published var useLocalSource = false
  26. @Published var localPort: Decimal = 0
  27. @Published var units: GlucoseUnits = .mgdL
  28. @Published var dia: Decimal = 6
  29. @Published var maxBasal: Decimal = 2
  30. @Published var maxBolus: Decimal = 10
  31. @Published var isConnectedToNS: Bool = false
  32. override func subscribe() {
  33. url = keychain.getValue(String.self, forKey: Config.urlKey) ?? ""
  34. secret = keychain.getValue(String.self, forKey: Config.secretKey) ?? ""
  35. units = settingsManager.settings.units
  36. dia = settingsManager.pumpSettings.insulinActionCurve
  37. maxBasal = settingsManager.pumpSettings.maxBasal
  38. maxBolus = settingsManager.pumpSettings.maxBolus
  39. subscribeSetting(\.isUploadEnabled, on: $isUploadEnabled) { isUploadEnabled = $0 }
  40. subscribeSetting(\.isDownloadEnabled, on: $isDownloadEnabled) { isDownloadEnabled = $0 }
  41. subscribeSetting(\.useLocalGlucoseSource, on: $useLocalSource) { useLocalSource = $0 }
  42. subscribeSetting(\.localGlucosePort, on: $localPort.map(Int.init)) { localPort = Decimal($0) }
  43. subscribeSetting(\.uploadGlucose, on: $uploadGlucose, initial: { uploadGlucose = $0 })
  44. isConnectedToNS = nightscoutAPI != nil
  45. $isUploadEnabled
  46. .dropFirst()
  47. .removeDuplicates()
  48. .sink { [weak self] enabled in
  49. guard let self = self else { return }
  50. if enabled {
  51. debug(.nightscout, "Upload has been enabled by the user.")
  52. Task {
  53. do {
  54. try await self.nightscoutManager.uploadProfiles()
  55. } catch {
  56. debug(
  57. .default,
  58. "\(DebuggingIdentifiers.failed) failed to upload profiles: \(error)"
  59. )
  60. }
  61. }
  62. } else {
  63. debug(.nightscout, "Upload has been disabled by the user.")
  64. }
  65. }
  66. .store(in: &lifetime)
  67. }
  68. func connect() {
  69. if let CheckURL = url.last, CheckURL == "/" {
  70. let fixedURL = url.dropLast()
  71. url = String(fixedURL)
  72. }
  73. guard let url = URL(string: url), self.url.hasPrefix("https://") else {
  74. message = "Invalid URL"
  75. isValidURL = false
  76. return
  77. }
  78. connecting = true
  79. isValidURL = true
  80. message = ""
  81. provider.checkConnection(url: url, secret: secret.isEmpty ? nil : secret)
  82. .receive(on: DispatchQueue.main)
  83. .sink { completion in
  84. switch completion {
  85. case .finished: break
  86. case let .failure(error):
  87. self.message = "Error: \(error.localizedDescription)"
  88. }
  89. self.connecting = false
  90. } receiveValue: {
  91. self.message = "Connected!"
  92. self.keychain.setValue(self.url, forKey: Config.urlKey)
  93. self.keychain.setValue(self.secret, forKey: Config.secretKey)
  94. self.connecting = true
  95. self.isConnectedToNS = self.nightscoutAPI != nil
  96. }
  97. .store(in: &lifetime)
  98. }
  99. private var nightscoutAPI: NightscoutAPI? {
  100. guard let urlString = keychain.getValue(String.self, forKey: NightscoutConfig.Config.urlKey),
  101. let url = URL(string: urlString),
  102. let secret = keychain.getValue(String.self, forKey: NightscoutConfig.Config.secretKey)
  103. else {
  104. return nil
  105. }
  106. return NightscoutAPI(url: url, secret: secret)
  107. }
  108. private func getMedianTarget(
  109. lowTargetValue: Decimal,
  110. lowTargetTime: String,
  111. highTarget: [NightscoutTimevalue],
  112. units: GlucoseUnits
  113. ) -> Decimal {
  114. if let idx = highTarget.firstIndex(where: { $0.time == lowTargetTime }) {
  115. let median = (lowTargetValue + highTarget[idx].value) / 2
  116. switch units {
  117. case .mgdL:
  118. return Decimal(round(Double(median)))
  119. case .mmolL:
  120. return Decimal(round(Double(median) * 10) / 10)
  121. }
  122. }
  123. return lowTargetValue
  124. }
  125. func backfillGlucose() async {
  126. await MainActor.run {
  127. backfilling = true
  128. }
  129. let glucose = await nightscoutManager.fetchGlucose(since: Date().addingTimeInterval(-1.days.timeInterval))
  130. if glucose.isNotEmpty {
  131. do {
  132. try await glucoseStorage.storeGlucose(glucose)
  133. Task.detached {
  134. await self.healthKitManager.uploadGlucose()
  135. }
  136. } catch let error as CoreDataError {
  137. debug(.nightscout, "Core Data error while storing backfilled glucose: \(error)")
  138. message = "Error: \(error.localizedDescription)"
  139. } catch {
  140. debug(.nightscout, "Unexpected error while storing backfilled glucose: \(error)")
  141. message = "Error: \(error.localizedDescription)"
  142. }
  143. } else {
  144. debug(.nightscout, "No glucose values found or fetched to backfill.")
  145. }
  146. await MainActor.run {
  147. self.backfilling = false
  148. }
  149. }
  150. func delete() {
  151. keychain.removeObject(forKey: Config.urlKey)
  152. keychain.removeObject(forKey: Config.secretKey)
  153. url = ""
  154. secret = ""
  155. isConnectedToNS = false
  156. }
  157. }
  158. }
  159. extension NightscoutConfig.StateModel: SettingsObserver {
  160. func settingsDidChange(_: TrioSettings) {
  161. units = settingsManager.settings.units
  162. }
  163. }