NightscoutConfigStateModel.swift 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624
  1. import Combine
  2. import CoreData
  3. <<<<<<< HEAD
  4. import G7SensorKit
  5. =======
  6. >>>>>>> 9672da256c317a314acc76d6e4f6e82cc174d133
  7. import LoopKit
  8. import SwiftDate
  9. import SwiftUI
  10. extension NightscoutConfig {
  11. final class StateModel: BaseStateModel<Provider> {
  12. @Injected() private var keychain: Keychain!
  13. @Injected() private var nightscoutManager: NightscoutManager!
  14. @Injected() private var glucoseStorage: GlucoseStorage!
  15. @Injected() private var healthKitManager: HealthKitManager!
  16. @Injected() private var cgmManager: FetchGlucoseManager!
  17. @Injected() private var storage: FileStorage!
  18. @Injected() var apsManager: APSManager!
  19. <<<<<<< HEAD
  20. let coredataContext = CoreDataStack.shared.newTaskContext()
  21. =======
  22. let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
  23. >>>>>>> 9672da256c317a314acc76d6e4f6e82cc174d133
  24. @Published var url = ""
  25. @Published var secret = ""
  26. @Published var message = ""
  27. @Published var connecting = false
  28. @Published var backfilling = false
  29. @Published var isUploadEnabled = false // Allow uploads
  30. @Published var isDownloadEnabled = false // Allow downloads
  31. @Published var uploadGlucose = true // Upload Glucose
  32. @Published var changeUploadGlucose = true // if plugin, need to be change in CGM configuration
  33. @Published var useLocalSource = false
  34. @Published var localPort: Decimal = 0
  35. <<<<<<< HEAD
  36. @Published var units: GlucoseUnits = .mgdL
  37. =======
  38. @Published var units: GlucoseUnits = .mmolL
  39. >>>>>>> 9672da256c317a314acc76d6e4f6e82cc174d133
  40. @Published var dia: Decimal = 6
  41. @Published var maxBasal: Decimal = 2
  42. @Published var maxBolus: Decimal = 10
  43. @Published var allowAnnouncements: Bool = false
  44. override func subscribe() {
  45. url = keychain.getValue(String.self, forKey: Config.urlKey) ?? ""
  46. secret = keychain.getValue(String.self, forKey: Config.secretKey) ?? ""
  47. units = settingsManager.settings.units
  48. dia = settingsManager.pumpSettings.insulinActionCurve
  49. maxBasal = settingsManager.pumpSettings.maxBasal
  50. maxBolus = settingsManager.pumpSettings.maxBolus
  51. <<<<<<< HEAD
  52. =======
  53. changeUploadGlucose = (cgmManager.cgmGlucoseSourceType != CGMType.plugin)
  54. >>>>>>> 9672da256c317a314acc76d6e4f6e82cc174d133
  55. subscribeSetting(\.allowAnnouncements, on: $allowAnnouncements) { allowAnnouncements = $0 }
  56. subscribeSetting(\.isUploadEnabled, on: $isUploadEnabled) { isUploadEnabled = $0 }
  57. subscribeSetting(\.isDownloadEnabled, on: $isDownloadEnabled) { isDownloadEnabled = $0 }
  58. subscribeSetting(\.useLocalGlucoseSource, on: $useLocalSource) { useLocalSource = $0 }
  59. subscribeSetting(\.localGlucosePort, on: $localPort.map(Int.init)) { localPort = Decimal($0) }
  60. subscribeSetting(\.uploadGlucose, on: $uploadGlucose, initial: { uploadGlucose = $0 })
  61. }
  62. func connect() {
  63. if let CheckURL = url.last, CheckURL == "/" {
  64. let fixedURL = url.dropLast()
  65. url = String(fixedURL)
  66. }
  67. guard let url = URL(string: url) else {
  68. message = "Invalid URL"
  69. return
  70. }
  71. connecting = true
  72. message = ""
  73. provider.checkConnection(url: url, secret: secret.isEmpty ? nil : secret)
  74. .receive(on: DispatchQueue.main)
  75. .sink { completion in
  76. switch completion {
  77. case .finished: break
  78. case let .failure(error):
  79. self.message = "Error: \(error.localizedDescription)"
  80. }
  81. self.connecting = false
  82. } receiveValue: {
  83. self.message = "Connected!"
  84. self.keychain.setValue(self.url, forKey: Config.urlKey)
  85. self.keychain.setValue(self.secret, forKey: Config.secretKey)
  86. }
  87. .store(in: &lifetime)
  88. }
  89. private var nightscoutAPI: NightscoutAPI? {
  90. guard let urlString = keychain.getValue(String.self, forKey: NightscoutConfig.Config.urlKey),
  91. let url = URL(string: urlString),
  92. let secret = keychain.getValue(String.self, forKey: NightscoutConfig.Config.secretKey)
  93. else {
  94. return nil
  95. }
  96. return NightscoutAPI(url: url, secret: secret)
  97. }
  98. <<<<<<< HEAD
  99. =======
  100. private func getMedianTarget(
  101. lowTargetValue: Decimal,
  102. lowTargetTime: String,
  103. highTarget: [NightscoutTimevalue],
  104. units: GlucoseUnits
  105. ) -> Decimal {
  106. if let idx = highTarget.firstIndex(where: { $0.time == lowTargetTime }) {
  107. let median = (lowTargetValue + highTarget[idx].value) / 2
  108. switch units {
  109. case .mgdL:
  110. return Decimal(round(Double(median)))
  111. case .mmolL:
  112. return Decimal(round(Double(median) * 10) / 10)
  113. }
  114. }
  115. return lowTargetValue
  116. }
  117. func importSettings() {
  118. guard let nightscout = nightscoutAPI else {
  119. saveError("Can't access nightscoutAPI")
  120. return
  121. }
  122. let group = DispatchGroup()
  123. group.enter()
  124. var error = ""
  125. let path = "/api/v1/profile.json"
  126. let timeout: TimeInterval = 60
  127. var components = URLComponents()
  128. components.scheme = nightscout.url.scheme
  129. components.host = nightscout.url.host
  130. components.port = nightscout.url.port
  131. components.path = path
  132. components.queryItems = [
  133. URLQueryItem(name: "count", value: "1")
  134. ]
  135. var url = URLRequest(url: components.url!)
  136. url.allowsConstrainedNetworkAccess = false
  137. url.timeoutInterval = timeout
  138. if let secret = nightscout.secret {
  139. url.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  140. }
  141. let task = URLSession.shared.dataTask(with: url) { data, response, error_ in
  142. if let error_ = error_ {
  143. print("Error occured: " + error_.localizedDescription)
  144. // handle error
  145. self.saveError("Error occured: " + error_.localizedDescription)
  146. error = error_.localizedDescription
  147. return
  148. }
  149. guard let httpResponse = response as? HTTPURLResponse,
  150. (200 ... 299).contains(httpResponse.statusCode)
  151. else {
  152. print("Error occured! " + error_.debugDescription)
  153. // handle error
  154. self.saveError(error_.debugDescription)
  155. return
  156. }
  157. let jsonDecoder = JSONCoding.decoder
  158. if let mimeType = httpResponse.mimeType, mimeType == "application/json",
  159. let data = data
  160. {
  161. do {
  162. let fetchedProfileStore = try jsonDecoder.decode([FetchedNightscoutProfileStore].self, from: data)
  163. let loop = fetchedProfileStore.first?.enteredBy.contains("Loop")
  164. guard let fetchedProfile: FetchedNightscoutProfile =
  165. (fetchedProfileStore.first?.store["default"] != nil) ?
  166. fetchedProfileStore.first?.store["default"] :
  167. fetchedProfileStore.first?.store["Default"]
  168. else {
  169. error = "\nCan't find the default Nightscout Profile."
  170. group.leave()
  171. return
  172. }
  173. guard fetchedProfile.units.contains(self.units.rawValue.prefix(4)) else {
  174. debug(
  175. .nightscout,
  176. "Mismatching glucose units in Nightscout and Pump Settings. Import settings aborted."
  177. )
  178. error = "\nMismatching glucose units in Nightscout and Pump Settings. Import settings aborted."
  179. group.leave()
  180. return
  181. }
  182. var areCRsOK = true
  183. let carbratios = fetchedProfile.carbratio
  184. .map { carbratio -> CarbRatioEntry in
  185. if carbratio.value <= 0 {
  186. error =
  187. "\nInvalid Carb Ratio settings in Nightscout.\n\nImport aborted. Please check your Nightscout Profile Carb Ratios Settings!"
  188. areCRsOK = false
  189. }
  190. return CarbRatioEntry(
  191. start: carbratio.time,
  192. offset: self.offset(carbratio.time) / 60,
  193. ratio: carbratio.value
  194. ) }
  195. let carbratiosProfile = CarbRatios(units: CarbUnit.grams, schedule: carbratios)
  196. guard areCRsOK else {
  197. group.leave()
  198. return
  199. }
  200. var areBasalsOK = true
  201. let pumpName = self.apsManager.pumpName.value
  202. let basals = fetchedProfile.basal
  203. .map { basal -> BasalProfileEntry in
  204. if pumpName != "Omnipod DASH", basal.value <= 0
  205. {
  206. error =
  207. "\nInvalid Nightcsout Basal Settings. Some or all of your basal settings are 0 U/h.\n\nImport aborted. Please check your Nightscout Profile Basal Settings before trying to import again. Import has been aborted.)"
  208. areBasalsOK = false
  209. }
  210. return BasalProfileEntry(
  211. start: basal.time,
  212. minutes: self.offset(basal.time) / 60,
  213. rate: basal.value
  214. ) }
  215. // DASH pumps can have 0U/h basal rates but don't import if total basals (24 hours) amount to 0 U.
  216. if pumpName == "Omnipod DASH", basals.map({ each in each.rate }).reduce(0, +) <= 0
  217. {
  218. error =
  219. "\nYour total Basal insulin amount to 0 U or lower in Nightscout Profile settings.\n\n Please check your Nightscout Profile Basal Settings before trying to import again. Import has been aborted.)"
  220. areBasalsOK = false
  221. }
  222. guard areBasalsOK else {
  223. group.leave()
  224. return
  225. }
  226. let sensitivities = fetchedProfile.sens.map { sensitivity -> InsulinSensitivityEntry in
  227. InsulinSensitivityEntry(
  228. sensitivity: sensitivity.value,
  229. offset: self.offset(sensitivity.time) / 60,
  230. start: sensitivity.time
  231. )
  232. }
  233. if sensitivities.filter({ $0.sensitivity <= 0 }).isNotEmpty {
  234. error =
  235. "\nInvalid Nightcsout Sensitivities Settings. \n\nImport aborted. Please check your Nightscout Profile Sensitivities Settings!"
  236. group.leave()
  237. return
  238. }
  239. let sensitivitiesProfile = InsulinSensitivities(
  240. units: self.units,
  241. userPrefferedUnits: self.units,
  242. sensitivities: sensitivities
  243. )
  244. let targets = fetchedProfile.target_low
  245. .map { target -> BGTargetEntry in
  246. let median = loop! ? self.getMedianTarget(
  247. lowTargetValue: target.value,
  248. lowTargetTime: target.time,
  249. highTarget: fetchedProfile.target_high,
  250. units: self.units
  251. ) : target.value
  252. return BGTargetEntry(
  253. low: median,
  254. high: median,
  255. start: target.time,
  256. offset: self.offset(target.time) / 60
  257. ) }
  258. let targetsProfile = BGTargets(
  259. units: self.units,
  260. userPrefferedUnits: self.units,
  261. targets: targets
  262. )
  263. // IS THERE A PUMP?
  264. guard let pump = self.apsManager.pumpManager else {
  265. self.storage.save(carbratiosProfile, as: OpenAPS.Settings.carbRatios)
  266. self.storage.save(basals, as: OpenAPS.Settings.basalProfile)
  267. self.storage.save(sensitivitiesProfile, as: OpenAPS.Settings.insulinSensitivities)
  268. self.storage.save(targetsProfile, as: OpenAPS.Settings.bgTargets)
  269. debug(
  270. .service,
  271. "Settings were imported but the Basals couldn't be saved to pump (No pump). Check your basal settings and tap ´Save on Pump´ to sync the new basal settings"
  272. )
  273. error =
  274. "\nSettings were imported but the Basals couldn't be saved to pump (No pump). Check your basal settings and tap ´Save on Pump´ to sync the new basal settings"
  275. group.leave()
  276. return
  277. }
  278. let syncValues = basals.map {
  279. RepeatingScheduleValue(startTime: TimeInterval($0.minutes * 60), value: Double($0.rate))
  280. }
  281. // SSAVE TO STORAGE. SAVE TO PUMP (LoopKit)
  282. pump.syncBasalRateSchedule(items: syncValues) { result in
  283. switch result {
  284. case .success:
  285. self.storage.save(basals, as: OpenAPS.Settings.basalProfile)
  286. self.storage.save(carbratiosProfile, as: OpenAPS.Settings.carbRatios)
  287. self.storage.save(sensitivitiesProfile, as: OpenAPS.Settings.insulinSensitivities)
  288. self.storage.save(targetsProfile, as: OpenAPS.Settings.bgTargets)
  289. debug(.service, "Settings have been imported and the Basals saved to pump!")
  290. // DIA. Save if changed.
  291. let dia = fetchedProfile.dia
  292. print("dia: " + dia.description)
  293. print("pump dia: " + self.dia.description)
  294. if dia != self.dia, dia >= 0 {
  295. let file = PumpSettings(
  296. insulinActionCurve: dia,
  297. maxBolus: self.maxBolus,
  298. maxBasal: self.maxBasal
  299. )
  300. self.storage.save(file, as: OpenAPS.Settings.settings)
  301. debug(.nightscout, "DIA setting updated to " + dia.description + " after a NS import.")
  302. }
  303. group.leave()
  304. case .failure:
  305. error =
  306. "\nSettings were imported but the Basals couldn't be saved to pump (communication error). Check your basal settings and tap ´Save on Pump´ to sync the new basal settings"
  307. debug(.service, "Basals couldn't be save to pump")
  308. group.leave()
  309. }
  310. }
  311. } catch let parsingError {
  312. print(parsingError)
  313. error = parsingError.localizedDescription
  314. group.leave()
  315. }
  316. }
  317. }
  318. task.resume()
  319. group.wait(wallTimeout: .now() + 5)
  320. group.notify(queue: .global(qos: .background)) {
  321. self.saveError(error)
  322. }
  323. }
  324. func offset(_ string: String) -> Int {
  325. let hours = Int(string.prefix(2)) ?? 0
  326. let minutes = Int(string.suffix(2)) ?? 0
  327. return ((hours * 60) + minutes) * 60
  328. }
  329. func saveError(_ string: String) {
  330. coredataContext.performAndWait {
  331. let saveToCoreData = ImportError(context: self.coredataContext)
  332. saveToCoreData.date = Date()
  333. saveToCoreData.error = string
  334. if coredataContext.hasChanges {
  335. try? coredataContext.save()
  336. }
  337. }
  338. }
  339. func backfillGlucose() {
  340. backfilling = true
  341. nightscoutManager.fetchGlucose(since: Date().addingTimeInterval(-1.days.timeInterval))
  342. .sink { [weak self] glucose in
  343. guard let self = self else { return }
  344. DispatchQueue.main.async {
  345. self.backfilling = false
  346. }
  347. >>>>>>> 9672da256c317a314acc76d6e4f6e82cc174d133
  348. func importSettings() {
  349. guard let nightscout = nightscoutAPI else {
  350. saveError("Can't access nightscoutAPI")
  351. return
  352. }
  353. let group = DispatchGroup()
  354. group.enter()
  355. var error = ""
  356. let path = "/api/v1/profile.json"
  357. let timeout: TimeInterval = 60
  358. var components = URLComponents()
  359. components.scheme = nightscout.url.scheme
  360. components.host = nightscout.url.host
  361. components.port = nightscout.url.port
  362. components.path = path
  363. components.queryItems = [
  364. URLQueryItem(name: "count", value: "1")
  365. ]
  366. var url = URLRequest(url: components.url!)
  367. url.allowsConstrainedNetworkAccess = false
  368. url.timeoutInterval = timeout
  369. if let secret = nightscout.secret {
  370. url.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
  371. }
  372. let task = URLSession.shared.dataTask(with: url) { data, response, error_ in
  373. if let error_ = error_ {
  374. print("Error occured: " + error_.localizedDescription)
  375. // handle error
  376. self.saveError("Error occured: " + error_.localizedDescription)
  377. error = error_.localizedDescription
  378. return
  379. }
  380. guard let httpResponse = response as? HTTPURLResponse,
  381. (200 ... 299).contains(httpResponse.statusCode)
  382. else {
  383. print("Error occured! " + error_.debugDescription)
  384. // handle error
  385. self.saveError(error_.debugDescription)
  386. return
  387. }
  388. let jsonDecoder = JSONCoding.decoder
  389. if let mimeType = httpResponse.mimeType, mimeType == "application/json",
  390. let data = data
  391. {
  392. do {
  393. let fetchedProfileStore = try jsonDecoder.decode([FetchedNightscoutProfileStore].self, from: data)
  394. guard let fetchedProfile: ScheduledNightscoutProfile = fetchedProfileStore.first?.store["default"]
  395. else {
  396. error = "\nCan't find the default Nightscout Profile."
  397. group.leave()
  398. return
  399. }
  400. guard fetchedProfile.units.contains(self.units.rawValue.prefix(4)) else {
  401. debug(
  402. .nightscout,
  403. "Mismatching glucose units in Nightscout and Pump Settings. Import settings aborted."
  404. )
  405. error = "\nMismatching glucose units in Nightscout and Pump Settings. Import settings aborted."
  406. group.leave()
  407. return
  408. }
  409. var areCRsOK = true
  410. let carbratios = fetchedProfile.carbratio
  411. .map { carbratio -> CarbRatioEntry in
  412. if carbratio.value <= 0 {
  413. error =
  414. "\nInvalid Carb Ratio settings in Nightscout.\n\nImport aborted. Please check your Nightscout Profile Carb Ratios Settings!"
  415. areCRsOK = false
  416. }
  417. return CarbRatioEntry(
  418. start: carbratio.time,
  419. offset: self.offset(carbratio.time) / 60,
  420. ratio: carbratio.value
  421. ) }
  422. let carbratiosProfile = CarbRatios(units: CarbUnit.grams, schedule: carbratios)
  423. guard areCRsOK else {
  424. group.leave()
  425. return
  426. }
  427. var areBasalsOK = true
  428. let pumpName = self.apsManager.pumpName.value
  429. let basals = fetchedProfile.basal
  430. .map { basal -> BasalProfileEntry in
  431. if pumpName != "Omnipod DASH", basal.value <= 0
  432. {
  433. error =
  434. "\nInvalid Nightcsout Basal Settings. Some or all of your basal settings are 0 U/h.\n\nImport aborted. Please check your Nightscout Profile Basal Settings before trying to import again. Import has been aborted.)"
  435. areBasalsOK = false
  436. }
  437. return BasalProfileEntry(
  438. start: basal.time,
  439. minutes: self.offset(basal.time) / 60,
  440. rate: basal.value
  441. ) }
  442. // DASH pumps can have 0U/h basal rates but don't import if total basals (24 hours) amount to 0 U.
  443. if pumpName == "Omnipod DASH", basals.map({ each in each.rate }).reduce(0, +) <= 0
  444. {
  445. error =
  446. "\nYour total Basal insulin amount to 0 U or lower in Nightscout Profile settings.\n\n Please check your Nightscout Profile Basal Settings before trying to import again. Import has been aborted.)"
  447. areBasalsOK = false
  448. }
  449. guard areBasalsOK else {
  450. group.leave()
  451. return
  452. }
  453. let sensitivities = fetchedProfile.sens.map { sensitivity -> InsulinSensitivityEntry in
  454. InsulinSensitivityEntry(
  455. sensitivity: self.units == .mmolL ? sensitivity.value : sensitivity.value.asMgdL,
  456. offset: self.offset(sensitivity.time) / 60,
  457. start: sensitivity.time
  458. )
  459. }
  460. if sensitivities.filter({ $0.sensitivity <= 0 }).isNotEmpty {
  461. error =
  462. "\nInvalid Nightcsout Sensitivities Settings. \n\nImport aborted. Please check your Nightscout Profile Sensitivities Settings!"
  463. group.leave()
  464. return
  465. }
  466. let sensitivitiesProfile = InsulinSensitivities(
  467. units: self.units,
  468. userPrefferedUnits: self.units,
  469. sensitivities: sensitivities
  470. )
  471. let targets = fetchedProfile.target_low
  472. .map { target -> BGTargetEntry in
  473. BGTargetEntry(
  474. low: self.units == .mmolL ? target.value : target.value.asMgdL,
  475. high: self.units == .mmolL ? target.value : target.value.asMgdL,
  476. start: target.time,
  477. offset: self.offset(target.time) / 60
  478. ) }
  479. let targetsProfile = BGTargets(
  480. units: self.units,
  481. userPrefferedUnits: self.units,
  482. targets: targets
  483. )
  484. // IS THERE A PUMP?
  485. guard let pump = self.apsManager.pumpManager else {
  486. self.storage.save(carbratiosProfile, as: OpenAPS.Settings.carbRatios)
  487. self.storage.save(basals, as: OpenAPS.Settings.basalProfile)
  488. self.storage.save(sensitivitiesProfile, as: OpenAPS.Settings.insulinSensitivities)
  489. self.storage.save(targetsProfile, as: OpenAPS.Settings.bgTargets)
  490. debug(
  491. .service,
  492. "Settings were imported but the Basals couldn't be saved to pump (No pump). Check your basal settings and tap ´Save on Pump´ to sync the new basal settings"
  493. )
  494. error =
  495. "\nSettings were imported but the Basals couldn't be saved to pump (No pump). Check your basal settings and tap ´Save on Pump´ to sync the new basal settings"
  496. group.leave()
  497. return
  498. }
  499. let syncValues = basals.map {
  500. RepeatingScheduleValue(startTime: TimeInterval($0.minutes * 60), value: Double($0.rate))
  501. }
  502. // SSAVE TO STORAGE. SAVE TO PUMP (LoopKit)
  503. pump.syncBasalRateSchedule(items: syncValues) { result in
  504. switch result {
  505. case .success:
  506. self.storage.save(basals, as: OpenAPS.Settings.basalProfile)
  507. self.storage.save(carbratiosProfile, as: OpenAPS.Settings.carbRatios)
  508. self.storage.save(sensitivitiesProfile, as: OpenAPS.Settings.insulinSensitivities)
  509. self.storage.save(targetsProfile, as: OpenAPS.Settings.bgTargets)
  510. debug(.service, "Settings have been imported and the Basals saved to pump!")
  511. // DIA. Save if changed.
  512. let dia = fetchedProfile.dia
  513. print("dia: " + dia.description)
  514. print("pump dia: " + self.dia.description)
  515. if dia != self.dia, dia >= 0 {
  516. let file = PumpSettings(
  517. insulinActionCurve: dia,
  518. maxBolus: self.maxBolus,
  519. maxBasal: self.maxBasal
  520. )
  521. self.storage.save(file, as: OpenAPS.Settings.settings)
  522. debug(.nightscout, "DIA setting updated to " + dia.description + " after a NS import.")
  523. }
  524. group.leave()
  525. case .failure:
  526. error =
  527. "\nSettings were imported but the Basals couldn't be saved to pump (communication error). Check your basal settings and tap ´Save on Pump´ to sync the new basal settings"
  528. debug(.service, "Basals couldn't be save to pump")
  529. group.leave()
  530. }
  531. }
  532. } catch let parsingError {
  533. print(parsingError)
  534. error = parsingError.localizedDescription
  535. group.leave()
  536. }
  537. }
  538. }
  539. task.resume()
  540. group.wait(wallTimeout: .now() + 5)
  541. group.notify(queue: .global(qos: .background)) {
  542. self.saveError(error)
  543. }
  544. }
  545. func offset(_ string: String) -> Int {
  546. let hours = Int(string.prefix(2)) ?? 0
  547. let minutes = Int(string.suffix(2)) ?? 0
  548. return ((hours * 60) + minutes) * 60
  549. }
  550. func saveError(_ string: String) {
  551. coredataContext.performAndWait {
  552. let saveToCoreData = ImportError(context: self.coredataContext)
  553. saveToCoreData.date = Date()
  554. saveToCoreData.error = string
  555. do {
  556. guard self.coredataContext.hasChanges else { return }
  557. try self.coredataContext.save()
  558. } catch {
  559. print(error.localizedDescription)
  560. }
  561. }
  562. }
  563. func backfillGlucose() async {
  564. backfilling = true
  565. let glucose = await nightscoutManager.fetchGlucose(since: Date().addingTimeInterval(-1.days.timeInterval))
  566. if glucose.isNotEmpty {
  567. await MainActor.run {
  568. self.backfilling = false
  569. self.healthKitManager.saveIfNeeded(bloodGlucose: glucose)
  570. self.glucoseStorage.storeGlucose(glucose)
  571. }
  572. } else {
  573. await MainActor.run {
  574. self.backfilling = false
  575. }
  576. }
  577. }
  578. func delete() {
  579. keychain.removeObject(forKey: Config.urlKey)
  580. keychain.removeObject(forKey: Config.secretKey)
  581. url = ""
  582. secret = ""
  583. }
  584. }
  585. }