NightscoutConfigStateModel.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. import CGMBLEKit
  2. import Combine
  3. import CoreData
  4. import G7SensorKit
  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 unlockmanager: UnlockManager!
  16. let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
  17. @Published var url = ""
  18. @Published var secret = ""
  19. @Published var message = ""
  20. @Published var connecting = false
  21. @Published var backfilling = false
  22. @Published var isUploadEnabled = false // Allow uploads
  23. @Published var uploadStats = false // Upload Statistics
  24. @Published var uploadGlucose = true // Upload Glucose
  25. @Published var useLocalSource = false
  26. @Published var localPort: Decimal = 0
  27. @Published var units: GlucoseUnits = .mmolL
  28. @Published var importedHasRun = false
  29. override func subscribe() {
  30. url = keychain.getValue(String.self, forKey: Config.urlKey) ?? ""
  31. secret = keychain.getValue(String.self, forKey: Config.secretKey) ?? ""
  32. units = settingsManager.settings.units
  33. subscribeSetting(\.isUploadEnabled, on: $isUploadEnabled) { isUploadEnabled = $0 }
  34. subscribeSetting(\.useLocalGlucoseSource, on: $useLocalSource) { useLocalSource = $0 }
  35. subscribeSetting(\.localGlucosePort, on: $localPort.map(Int.init)) { localPort = Decimal($0) }
  36. subscribeSetting(\.uploadStats, on: $uploadStats) { uploadStats = $0 }
  37. subscribeSetting(\.uploadGlucose, on: $uploadGlucose, initial: { uploadGlucose = $0 }, didSet: { val in
  38. if let cgmManagerG5 = self.cgmManager.glucoseSource.cgmManager as? G5CGMManager {
  39. cgmManagerG5.shouldSyncToRemoteService = val
  40. }
  41. if let cgmManagerG6 = self.cgmManager.glucoseSource.cgmManager as? G6CGMManager {
  42. cgmManagerG6.shouldSyncToRemoteService = val
  43. }
  44. if let cgmManagerG7 = self.cgmManager.glucoseSource.cgmManager as? G7CGMManager {
  45. cgmManagerG7.uploadReadings = val
  46. }
  47. })
  48. }
  49. func connect() {
  50. guard let url = URL(string: url) else {
  51. message = "Invalid URL"
  52. return
  53. }
  54. connecting = true
  55. message = ""
  56. provider.checkConnection(url: url, secret: secret.isEmpty ? nil : secret)
  57. .receive(on: DispatchQueue.main)
  58. .sink { completion in
  59. switch completion {
  60. case .finished: break
  61. case let .failure(error):
  62. self.message = "Error: \(error.localizedDescription)"
  63. }
  64. self.connecting = false
  65. } receiveValue: {
  66. self.message = "Connected!"
  67. self.keychain.setValue(self.url, forKey: Config.urlKey)
  68. self.keychain.setValue(self.secret, forKey: Config.secretKey)
  69. }
  70. .store(in: &lifetime)
  71. }
  72. private var nightscoutAPI: NightscoutAPI? {
  73. guard let urlString = keychain.getValue(String.self, forKey: NightscoutConfig.Config.urlKey),
  74. let url = URL(string: urlString),
  75. let secret = keychain.getValue(String.self, forKey: NightscoutConfig.Config.secretKey)
  76. else {
  77. return nil
  78. }
  79. return NightscoutAPI(url: url, secret: secret)
  80. }
  81. func startImport() {
  82. unlockmanager.unlock()
  83. .sink { _ in } receiveValue: { [weak self] _ in
  84. guard let self = self else { return }
  85. importSettings()
  86. importedHasRun = true
  87. }
  88. .store(in: &lifetime)
  89. }
  90. func importSettings() {
  91. guard let nightscout = nightscoutAPI else {
  92. saveError("Can't access nightscoutAPI")
  93. return
  94. }
  95. let group = DispatchGroup()
  96. group.enter()
  97. var error = ""
  98. let path = "/api/v1/profile.json"
  99. let timeout: TimeInterval = 60
  100. var components = URLComponents()
  101. components.scheme = nightscout.url.scheme
  102. components.host = nightscout.url.host
  103. components.port = nightscout.url.port
  104. components.path = path
  105. components.queryItems = [
  106. URLQueryItem(name: "count", value: "1")
  107. ]
  108. var url = URLRequest(url: components.url!)
  109. url.allowsConstrainedNetworkAccess = false
  110. url.timeoutInterval = timeout
  111. if let secret = nightscout.secret {
  112. url.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  113. }
  114. let task = URLSession.shared.dataTask(with: url) { data, response, error_ in
  115. if let error_ = error_ {
  116. print("Error occured: " + error_.localizedDescription)
  117. // handle error
  118. self.saveError("Error occured: " + error_.localizedDescription)
  119. error = error_.localizedDescription
  120. return
  121. }
  122. guard let httpResponse = response as? HTTPURLResponse,
  123. (200 ... 299).contains(httpResponse.statusCode)
  124. else {
  125. print("Error occured! " + error_.debugDescription)
  126. // handle error
  127. self.saveError(error_.debugDescription)
  128. return
  129. }
  130. let jsonDecoder = JSONCoding.decoder
  131. if let mimeType = httpResponse.mimeType, mimeType == "application/json",
  132. let data = data
  133. {
  134. do {
  135. let fetchedProfileStore = try jsonDecoder.decode([FetchedNightscoutProfileStore].self, from: data)
  136. guard let fetchedProfile: ScheduledNightscoutProfile = fetchedProfileStore.first?.store["default"]
  137. else {
  138. error = "Can't find the default Nightscout Profile."
  139. group.leave()
  140. return
  141. }
  142. guard fetchedProfile.units.contains(self.units.rawValue.prefix(4)) else {
  143. debug(
  144. .nightscout,
  145. "Mismatching glucose units in Nightscout and Pump Settings. Import settings aborted."
  146. )
  147. error = "Mismatching glucose units in Nightscout and Pump Settings. Import settings aborted."
  148. group.leave()
  149. return
  150. }
  151. let carbratios = fetchedProfile.carbratio
  152. .map { carbratio -> CarbRatioEntry in
  153. CarbRatioEntry(
  154. start: carbratio.time,
  155. offset: (carbratio.timeAsSeconds ?? self.offset(carbratio.time)) / 60,
  156. ratio: carbratio.value
  157. ) }
  158. let carbratiosProfile = CarbRatios(units: CarbUnit.grams, schedule: carbratios)
  159. let basals = fetchedProfile.basal
  160. .map { basal -> BasalProfileEntry in
  161. BasalProfileEntry(
  162. start: basal.time,
  163. minutes: (basal.timeAsSeconds ?? self.offset(basal.time)) / 60,
  164. rate: basal.value
  165. ) }
  166. let sensitivities = fetchedProfile.sens.map { sensitivity -> InsulinSensitivityEntry in
  167. InsulinSensitivityEntry(
  168. sensitivity: self.units == .mmolL ? sensitivity.value : sensitivity.value.asMgdL,
  169. offset: (sensitivity.timeAsSeconds ?? self.offset(sensitivity.time)) / 60,
  170. start: sensitivity.time
  171. ) }
  172. let sensitivitiesProfile = InsulinSensitivities(
  173. units: self.units,
  174. userPrefferedUnits: self.units,
  175. sensitivities: sensitivities
  176. )
  177. let targets = fetchedProfile.target_low
  178. .map { target -> BGTargetEntry in
  179. BGTargetEntry(
  180. low: self.units == .mmolL ? target.value : target.value.asMgdL,
  181. high: self.units == .mmolL ? target.value : target.value.asMgdL,
  182. start: target.time,
  183. offset: (target.timeAsSeconds ?? self.offset(target.time)) / 60
  184. ) }
  185. let targetsProfile = BGTargets(
  186. units: self.units,
  187. userPrefferedUnits: self.units,
  188. targets: targets
  189. )
  190. self.storage.save(carbratiosProfile, as: OpenAPS.Settings.carbRatios)
  191. self.storage.save(basals, as: OpenAPS.Settings.basalProfile)
  192. self.storage.save(sensitivitiesProfile, as: OpenAPS.Settings.insulinSensitivities)
  193. self.storage.save(targetsProfile, as: OpenAPS.Settings.bgTargets)
  194. group.leave()
  195. } catch let parsingError {
  196. print(parsingError)
  197. }
  198. }
  199. }
  200. task.resume()
  201. group.wait(wallTimeout: .now() + 5)
  202. group.notify(queue: .global(qos: .background)) {
  203. self.saveError(error)
  204. }
  205. }
  206. func offset(_ string: String) -> Int {
  207. let hours = Int(string.prefix(2)) ?? 0
  208. let minutes = Int(string.suffix(2)) ?? 0
  209. return hours * 60 + minutes * 60
  210. }
  211. func saveError(_ string: String) {
  212. coredataContext.performAndWait {
  213. let saveToCoreData = ImportError(context: self.coredataContext)
  214. saveToCoreData.date = Date()
  215. saveToCoreData.error = string
  216. if coredataContext.hasChanges {
  217. try? coredataContext.save()
  218. }
  219. }
  220. }
  221. func backfillGlucose() {
  222. backfilling = true
  223. nightscoutManager.fetchGlucose(since: Date().addingTimeInterval(-1.days.timeInterval))
  224. .sink { [weak self] glucose in
  225. guard let self = self else { return }
  226. DispatchQueue.main.async {
  227. self.backfilling = false
  228. }
  229. guard glucose.isNotEmpty else { return }
  230. self.healthKitManager.saveIfNeeded(bloodGlucose: glucose)
  231. self.glucoseStorage.storeGlucose(glucose)
  232. }
  233. .store(in: &lifetime)
  234. }
  235. func delete() {
  236. keychain.removeObject(forKey: Config.urlKey)
  237. keychain.removeObject(forKey: Config.secretKey)
  238. url = ""
  239. secret = ""
  240. }
  241. }
  242. }