NightscoutManager.swift 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980
  1. import Combine
  2. import CoreData
  3. import Foundation
  4. import LoopKitUI
  5. import Swinject
  6. import UIKit
  7. protocol NightscoutManager: GlucoseSource {
  8. func fetchGlucose(since date: Date) async -> [BloodGlucose]
  9. func fetchCarbs() async -> [CarbsEntry]
  10. func fetchTempTargets() async -> [TempTarget]
  11. func fetchAnnouncements() -> AnyPublisher<[Announcement], Never>
  12. func deleteCarbs(withID id: String) async
  13. func deleteInsulin(withID id: String) async
  14. func deleteManualGlucose(withID id: String) async
  15. func uploadStatus() async
  16. func uploadGlucose() async
  17. func uploadManualGlucose() async
  18. func uploadStatistics(dailystat: Statistics) async
  19. func uploadPreferences(_ preferences: Preferences)
  20. func uploadProfileAndSettings(_: Bool)
  21. var cgmURL: URL? { get }
  22. }
  23. final class BaseNightscoutManager: NightscoutManager, Injectable {
  24. @Injected() private var keychain: Keychain!
  25. @Injected() private var determinationStorage: DeterminationStorage!
  26. @Injected() private var glucoseStorage: GlucoseStorage!
  27. @Injected() private var tempTargetsStorage: TempTargetsStorage!
  28. @Injected() private var overridesStorage: OverrideStorage!
  29. @Injected() private var carbsStorage: CarbsStorage!
  30. @Injected() private var pumpHistoryStorage: PumpHistoryStorage!
  31. @Injected() private var storage: FileStorage!
  32. @Injected() private var announcementsStorage: AnnouncementsStorage!
  33. @Injected() private var settingsManager: SettingsManager!
  34. @Injected() private var broadcaster: Broadcaster!
  35. @Injected() private var reachabilityManager: ReachabilityManager!
  36. @Injected() var healthkitManager: HealthKitManager!
  37. private let processQueue = DispatchQueue(label: "BaseNetworkManager.processQueue")
  38. private var ping: TimeInterval?
  39. private var backgroundContext = CoreDataStack.shared.newTaskContext()
  40. private var lifetime = Lifetime()
  41. private var isNetworkReachable: Bool {
  42. reachabilityManager.isReachable
  43. }
  44. private var isUploadEnabled: Bool {
  45. settingsManager.settings.isUploadEnabled
  46. }
  47. private var isUploadGlucoseEnabled: Bool {
  48. settingsManager.settings.uploadGlucose
  49. }
  50. private var nightscoutAPI: NightscoutAPI? {
  51. guard let urlString = keychain.getValue(String.self, forKey: NightscoutConfig.Config.urlKey),
  52. let url = URL(string: urlString),
  53. let secret = keychain.getValue(String.self, forKey: NightscoutConfig.Config.secretKey)
  54. else {
  55. return nil
  56. }
  57. return NightscoutAPI(url: url, secret: secret)
  58. }
  59. private var lastEnactedDetermination: Determination?
  60. private var lastSuggestedDetermination: Determination?
  61. private var coreDataObserver: CoreDataObserver?
  62. init(resolver: Resolver) {
  63. injectServices(resolver)
  64. subscribe()
  65. coreDataObserver = CoreDataObserver()
  66. registerHandlers()
  67. }
  68. private func subscribe() {
  69. _ = reachabilityManager.startListening(onQueue: processQueue) { status in
  70. debug(.nightscout, "Network status: \(status)")
  71. }
  72. }
  73. private func registerHandlers() {
  74. coreDataObserver?.registerHandler(for: "OrefDetermination") { [weak self] in
  75. guard let self = self else { return }
  76. Task.detached {
  77. await self.uploadStatus()
  78. }
  79. }
  80. coreDataObserver?.registerHandler(for: "OverrideStored") { [weak self] in
  81. guard let self = self else { return }
  82. Task.detached {
  83. await self.uploadOverrides()
  84. }
  85. }
  86. coreDataObserver?.registerHandler(for: "OverrideRunStored") { [weak self] in
  87. guard let self = self else { return }
  88. Task.detached {
  89. await self.uploadOverrides()
  90. }
  91. }
  92. coreDataObserver?.registerHandler(for: "PumpEventStored") { [weak self] in
  93. guard let self = self else { return }
  94. Task.detached {
  95. await self.uploadPumpHistory()
  96. }
  97. }
  98. coreDataObserver?.registerHandler(for: "CarbEntryStored") { [weak self] in
  99. guard let self = self else { return }
  100. Task.detached {
  101. await self.uploadCarbs()
  102. }
  103. }
  104. coreDataObserver?.registerHandler(for: "GlucoseStored") { [weak self] in
  105. guard let self = self else { return }
  106. Task.detached {
  107. await self.uploadManualGlucose()
  108. }
  109. }
  110. }
  111. func sourceInfo() -> [String: Any]? {
  112. if let ping = ping {
  113. return [GlucoseSourceKey.nightscoutPing.rawValue: ping]
  114. }
  115. return nil
  116. }
  117. var cgmURL: URL? {
  118. if let url = settingsManager.settings.cgm.appURL {
  119. return url
  120. }
  121. let useLocal = settingsManager.settings.useLocalGlucoseSource
  122. let maybeNightscout = useLocal
  123. ? NightscoutAPI(url: URL(string: "http://127.0.0.1:\(settingsManager.settings.localGlucosePort)")!)
  124. : nightscoutAPI
  125. return maybeNightscout?.url
  126. }
  127. func fetchGlucose(since date: Date) async -> [BloodGlucose] {
  128. let useLocal = settingsManager.settings.useLocalGlucoseSource
  129. ping = nil
  130. if !useLocal {
  131. guard isNetworkReachable else {
  132. return []
  133. }
  134. }
  135. let maybeNightscout = useLocal
  136. ? NightscoutAPI(url: URL(string: "http://127.0.0.1:\(settingsManager.settings.localGlucosePort)")!)
  137. : nightscoutAPI
  138. guard let nightscout = maybeNightscout else {
  139. return []
  140. }
  141. let startDate = Date()
  142. do {
  143. let glucose = try await nightscout.fetchLastGlucose(sinceDate: date)
  144. if glucose.isNotEmpty {
  145. ping = Date().timeIntervalSince(startDate)
  146. }
  147. return glucose
  148. } catch {
  149. print(error.localizedDescription)
  150. return []
  151. }
  152. }
  153. // MARK: - GlucoseSource
  154. var glucoseManager: FetchGlucoseManager?
  155. var cgmManager: CGMManagerUI?
  156. func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
  157. Future { promise in
  158. Task {
  159. let glucoseData = await self.fetchGlucose(since: self.glucoseStorage.syncDate())
  160. promise(.success(glucoseData))
  161. }
  162. }
  163. .eraseToAnyPublisher()
  164. }
  165. func fetchIfNeeded() -> AnyPublisher<[BloodGlucose], Never> {
  166. fetch(nil)
  167. }
  168. func fetchCarbs() async -> [CarbsEntry] {
  169. guard let nightscout = nightscoutAPI, isNetworkReachable else {
  170. return []
  171. }
  172. let since = carbsStorage.syncDate()
  173. do {
  174. let carbs = try await nightscout.fetchCarbs(sinceDate: since)
  175. return carbs
  176. } catch {
  177. debug(.nightscout, "Error fetching carbs: \(error.localizedDescription)")
  178. return []
  179. }
  180. }
  181. func fetchTempTargets() async -> [TempTarget] {
  182. guard let nightscout = nightscoutAPI, isNetworkReachable else {
  183. return []
  184. }
  185. let since = tempTargetsStorage.syncDate()
  186. do {
  187. let tempTargets = try await nightscout.fetchTempTargets(sinceDate: since)
  188. return tempTargets
  189. } catch {
  190. debug(.nightscout, "Error fetching temp targets: \(error.localizedDescription)")
  191. return []
  192. }
  193. }
  194. func fetchAnnouncements() -> AnyPublisher<[Announcement], Never> {
  195. guard let nightscout = nightscoutAPI, isNetworkReachable else {
  196. return Just([]).eraseToAnyPublisher()
  197. }
  198. let since = announcementsStorage.syncDate()
  199. return nightscout.fetchAnnouncement(sinceDate: since)
  200. .replaceError(with: [])
  201. .eraseToAnyPublisher()
  202. }
  203. func deleteCarbs(withID id: String) async {
  204. guard let nightscout = nightscoutAPI, isUploadEnabled else { return }
  205. // TODO: - healthkit rewrite, deletion of FPUs
  206. // healthkitManager.deleteCarbs(syncID: arg1, fpuID: arg2)
  207. do {
  208. try await nightscout.deleteCarbs(withId: id)
  209. debug(.nightscout, "Carbs deleted")
  210. } catch {
  211. debug(
  212. .nightscout,
  213. "\(DebuggingIdentifiers.failed) Failed to delete Carbs from Nightscout with error: \(error.localizedDescription)"
  214. )
  215. }
  216. }
  217. func deleteInsulin(withID id: String) async {
  218. guard let nightscout = nightscoutAPI, isUploadEnabled else { return }
  219. do {
  220. try await nightscout.deleteInsulin(withId: id)
  221. debug(.nightscout, "Insulin deleted")
  222. } catch {
  223. debug(
  224. .nightscout,
  225. "\(DebuggingIdentifiers.failed) Failed to delete Insulin from Nightscout with error: \(error.localizedDescription)"
  226. )
  227. }
  228. }
  229. func deleteManualGlucose(withID id: String) async {
  230. guard let nightscout = nightscoutAPI, isUploadEnabled else { return }
  231. do {
  232. try await nightscout.deleteManualGlucose(withId: id)
  233. } catch {
  234. debug(
  235. .nightscout,
  236. "\(DebuggingIdentifiers.failed) Failed to delete Manual Glucose from Nightscout with error: \(error.localizedDescription)"
  237. )
  238. }
  239. }
  240. func uploadStatistics(dailystat: Statistics) async {
  241. let stats = NightscoutStatistics(dailystats: dailystat)
  242. guard let nightscout = nightscoutAPI, isUploadEnabled else {
  243. return
  244. }
  245. do {
  246. try await nightscout.uploadStats(stats)
  247. debug(.nightscout, "Statistics uploaded")
  248. } catch {
  249. debug(.nightscout, error.localizedDescription)
  250. }
  251. }
  252. func uploadPreferences(_ preferences: Preferences) {
  253. let prefs = NightscoutPreferences(
  254. preferences: settingsManager.preferences
  255. )
  256. guard let nightscout = nightscoutAPI, isUploadEnabled else {
  257. return
  258. }
  259. processQueue.async {
  260. nightscout.uploadPrefs(prefs)
  261. .sink { completion in
  262. switch completion {
  263. case .finished:
  264. debug(.nightscout, "Preferences uploaded")
  265. self.storage.save(preferences, as: OpenAPS.Nightscout.uploadedPreferences)
  266. case let .failure(error):
  267. debug(.nightscout, error.localizedDescription)
  268. }
  269. } receiveValue: {}
  270. .store(in: &self.lifetime)
  271. }
  272. }
  273. func uploadSettings(_ settings: FreeAPSSettings) {
  274. let sets = NightscoutSettings(
  275. settings: settingsManager.settings
  276. )
  277. guard let nightscout = nightscoutAPI, isUploadEnabled else {
  278. return
  279. }
  280. processQueue.async {
  281. nightscout.uploadSettings(sets)
  282. .sink { completion in
  283. switch completion {
  284. case .finished:
  285. debug(.nightscout, "Settings uploaded")
  286. self.storage.save(settings, as: OpenAPS.Nightscout.uploadedSettings)
  287. case let .failure(error):
  288. debug(.nightscout, error.localizedDescription)
  289. }
  290. } receiveValue: {}
  291. .store(in: &self.lifetime)
  292. }
  293. }
  294. private func fetchBattery() async -> Battery {
  295. await backgroundContext.perform {
  296. do {
  297. let results = try self.backgroundContext.fetch(OpenAPS_Battery.fetch(NSPredicate.predicateFor30MinAgo))
  298. if let last = results.first {
  299. let percent: Int? = Int(last.percent)
  300. let voltage: Decimal? = last.voltage as Decimal?
  301. let status: String? = last.status
  302. let display: Bool? = last.display
  303. if let percent = percent, let voltage = voltage, let status = status, let display = display {
  304. debugPrint(
  305. "Home State Model: \(#function) \(DebuggingIdentifiers.succeeded) setup battery from core data successfully"
  306. )
  307. return Battery(
  308. percent: percent,
  309. voltage: voltage,
  310. string: BatteryState(rawValue: status) ?? BatteryState.normal,
  311. display: display
  312. )
  313. }
  314. }
  315. return Battery(percent: 100, voltage: 100, string: BatteryState.normal, display: false)
  316. } catch {
  317. debugPrint(
  318. "Home State Model: \(#function) \(DebuggingIdentifiers.failed) failed to setup battery from core data"
  319. )
  320. return Battery(percent: 100, voltage: 100, string: BatteryState.normal, display: false)
  321. }
  322. }
  323. }
  324. func uploadStatus() async {
  325. guard let nightscout = nightscoutAPI, isUploadEnabled else {
  326. debug(.nightscout, "NS API not available or upload disabled. Aborting NS Status upload.")
  327. return
  328. }
  329. // Suggested/ Enacted
  330. async let enactedDeterminationID = determinationStorage
  331. .fetchLastDeterminationObjectID(predicate: NSPredicate.enactedDeterminationsNotYetUploadedToNightscout)
  332. async let suggestedDeterminationID = determinationStorage
  333. .fetchLastDeterminationObjectID(predicate: NSPredicate.suggestedDeterminationsNotYetUploadedToNightscout)
  334. // OpenAPS Status
  335. async let fetchedBattery = fetchBattery()
  336. async let fetchedReservoir = Decimal(from: storage.retrieveRawAsync(OpenAPS.Monitor.reservoir) ?? "0")
  337. async let fetchedIOBEntry = storage.retrieveAsync(OpenAPS.Monitor.iob, as: [IOBEntry].self)
  338. async let fetchedPumpStatus = storage.retrieveAsync(OpenAPS.Monitor.status, as: PumpStatus.self)
  339. let (fetchedEnactedDetermination, fetchedSuggestedDetermination) = await (
  340. determinationStorage.getOrefDeterminationNotYetUploadedToNightscout(enactedDeterminationID),
  341. determinationStorage.getOrefDeterminationNotYetUploadedToNightscout(suggestedDeterminationID)
  342. )
  343. // Guard to ensure both determinations are not nil
  344. guard fetchedEnactedDetermination != nil || fetchedSuggestedDetermination != nil else {
  345. debug(
  346. .nightscout,
  347. "Both fetchedEnactedDetermination and fetchedSuggestedDetermination are nil. Aborting NS Status upload."
  348. )
  349. return
  350. }
  351. // Unwrap fetchedSuggestedDetermination and manipulate the timestamp field to ensure deliverAt and timestamp for a suggestion truly match!
  352. var modifiedSuggestedDetermination = fetchedSuggestedDetermination
  353. if var suggestion = fetchedSuggestedDetermination {
  354. suggestion.timestamp = suggestion.deliverAt
  355. // Check whether the last suggestion that was uploaded is the same that is fetched again when we are attempting to upload the enacted determination
  356. // Apparently we are too fast; so the flag update is not fast enough to have the predicate filter last suggestion out
  357. // If this check is truthy, set suggestion to nil so it's not uploaded again
  358. if let lastSuggested = lastSuggestedDetermination, lastSuggested.deliverAt == suggestion.deliverAt {
  359. modifiedSuggestedDetermination = nil
  360. } else {
  361. modifiedSuggestedDetermination = suggestion
  362. }
  363. }
  364. // Gather all relevant data for OpenAPS Status
  365. let iob = await fetchedIOBEntry
  366. let openapsStatus = OpenAPSStatus(
  367. iob: iob?.first,
  368. suggested: modifiedSuggestedDetermination,
  369. enacted: settingsManager.settings.closedLoop ? fetchedEnactedDetermination : nil,
  370. version: "0.7.1"
  371. )
  372. // Gather all relevant data for NS Status
  373. let battery = await fetchedBattery
  374. let reservoir = await fetchedReservoir
  375. let pumpStatus = await fetchedPumpStatus
  376. let pump = NSPumpStatus(
  377. clock: Date(),
  378. battery: battery,
  379. reservoir: reservoir != 0xDEAD_BEEF ? reservoir : nil,
  380. status: pumpStatus
  381. )
  382. let device = await UIDevice.current
  383. let uploader = await Uploader(batteryVoltage: nil, battery: Int(device.batteryLevel * 100))
  384. let status = NightscoutStatus(
  385. device: NightscoutTreatment.local,
  386. openaps: openapsStatus,
  387. pump: pump,
  388. uploader: uploader
  389. )
  390. do {
  391. try await nightscout.uploadStatus(status)
  392. debug(.nightscout, "Status uploaded")
  393. if let enacted = fetchedEnactedDetermination {
  394. await updateOrefDeterminationAsUploaded([enacted])
  395. }
  396. if let suggested = fetchedSuggestedDetermination {
  397. await updateOrefDeterminationAsUploaded([suggested])
  398. }
  399. lastEnactedDetermination = fetchedEnactedDetermination
  400. lastSuggestedDetermination = fetchedSuggestedDetermination
  401. debug(.nightscout, "NSDeviceStatus with Determination uploaded")
  402. } catch {
  403. debug(.nightscout, error.localizedDescription)
  404. }
  405. Task.detached {
  406. await self.uploadPodAge()
  407. }
  408. }
  409. private func updateOrefDeterminationAsUploaded(_ determination: [Determination]) async {
  410. await backgroundContext.perform {
  411. let ids = determination.map(\.id) as NSArray
  412. print("\(DebuggingIdentifiers.inProgress) ids: \(ids)")
  413. let fetchRequest: NSFetchRequest<OrefDetermination> = OrefDetermination.fetchRequest()
  414. fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
  415. do {
  416. let results = try self.backgroundContext.fetch(fetchRequest)
  417. print("\(DebuggingIdentifiers.inProgress) results: \(results)")
  418. for result in results {
  419. result.isUploadedToNS = true
  420. }
  421. guard self.backgroundContext.hasChanges else { return }
  422. try self.backgroundContext.save()
  423. } catch let error as NSError {
  424. debugPrint(
  425. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToNS: \(error.userInfo)"
  426. )
  427. }
  428. }
  429. }
  430. func uploadPodAge() async {
  431. let uploadedPodAge = storage.retrieve(OpenAPS.Nightscout.uploadedPodAge, as: [NightscoutTreatment].self) ?? []
  432. if let podAge = storage.retrieve(OpenAPS.Monitor.podAge, as: Date.self),
  433. uploadedPodAge.last?.createdAt == nil || podAge != uploadedPodAge.last!.createdAt!
  434. {
  435. let siteTreatment = NightscoutTreatment(
  436. duration: nil,
  437. rawDuration: nil,
  438. rawRate: nil,
  439. absolute: nil,
  440. rate: nil,
  441. eventType: .nsSiteChange,
  442. createdAt: podAge,
  443. enteredBy: NightscoutTreatment.local,
  444. bolus: nil,
  445. insulin: nil,
  446. notes: nil,
  447. carbs: nil,
  448. fat: nil,
  449. protein: nil,
  450. targetTop: nil,
  451. targetBottom: nil
  452. )
  453. await uploadTreatments([siteTreatment], fileToSave: OpenAPS.Nightscout.uploadedPodAge)
  454. }
  455. }
  456. func uploadProfileAndSettings(_ force: Bool) {
  457. guard let sensitivities = storage.retrieve(OpenAPS.Settings.insulinSensitivities, as: InsulinSensitivities.self) else {
  458. debug(.nightscout, "NightscoutManager uploadProfile: error loading insulinSensitivities")
  459. return
  460. }
  461. guard let settings = storage.retrieve(OpenAPS.FreeAPS.settings, as: FreeAPSSettings.self) else {
  462. debug(.nightscout, "NightscoutManager uploadProfile: error loading settings")
  463. return
  464. }
  465. guard let preferences = storage.retrieve(OpenAPS.Settings.preferences, as: Preferences.self) else {
  466. debug(.nightscout, "NightscoutManager uploadProfile: error loading preferences")
  467. return
  468. }
  469. guard let targets = storage.retrieve(OpenAPS.Settings.bgTargets, as: BGTargets.self) else {
  470. debug(.nightscout, "NightscoutManager uploadProfile: error loading bgTargets")
  471. return
  472. }
  473. guard let carbRatios = storage.retrieve(OpenAPS.Settings.carbRatios, as: CarbRatios.self) else {
  474. debug(.nightscout, "NightscoutManager uploadProfile: error loading carbRatios")
  475. return
  476. }
  477. guard let basalProfile = storage.retrieve(OpenAPS.Settings.basalProfile, as: [BasalProfileEntry].self) else {
  478. debug(.nightscout, "NightscoutManager uploadProfile: error loading basalProfile")
  479. return
  480. }
  481. let sens = sensitivities.sensitivities.map { item -> NightscoutTimevalue in
  482. NightscoutTimevalue(
  483. time: String(item.start.prefix(5)),
  484. value: item.sensitivity,
  485. timeAsSeconds: item.offset * 60
  486. )
  487. }
  488. let target_low = targets.targets.map { item -> NightscoutTimevalue in
  489. NightscoutTimevalue(
  490. time: String(item.start.prefix(5)),
  491. value: item.low,
  492. timeAsSeconds: item.offset * 60
  493. )
  494. }
  495. let target_high = targets.targets.map { item -> NightscoutTimevalue in
  496. NightscoutTimevalue(
  497. time: String(item.start.prefix(5)),
  498. value: item.high,
  499. timeAsSeconds: item.offset * 60
  500. )
  501. }
  502. let cr = carbRatios.schedule.map { item -> NightscoutTimevalue in
  503. NightscoutTimevalue(
  504. time: String(item.start.prefix(5)),
  505. value: item.ratio,
  506. timeAsSeconds: item.offset * 60
  507. )
  508. }
  509. let basal = basalProfile.map { item -> NightscoutTimevalue in
  510. NightscoutTimevalue(
  511. time: String(item.start.prefix(5)),
  512. value: item.rate,
  513. timeAsSeconds: item.minutes * 60
  514. )
  515. }
  516. var nsUnits = ""
  517. switch settingsManager.settings.units {
  518. case .mgdL:
  519. nsUnits = "mg/dl"
  520. case .mmolL:
  521. nsUnits = "mmol"
  522. }
  523. var carbs_hr: Decimal = 0
  524. if let isf = sensitivities.sensitivities.map(\.sensitivity).first,
  525. let cr = carbRatios.schedule.map(\.ratio).first,
  526. isf > 0, cr > 0
  527. {
  528. // CarbImpact -> Carbs/hr = CI [mg/dl/5min] * 12 / ISF [mg/dl/U] * CR [g/U]
  529. carbs_hr = settingsManager.preferences.min5mCarbimpact * 12 / isf * cr
  530. if settingsManager.settings.units == .mmolL {
  531. carbs_hr = carbs_hr * GlucoseUnits.exchangeRate
  532. }
  533. // No, Decimal has no rounding function.
  534. carbs_hr = Decimal(round(Double(carbs_hr) * 10.0)) / 10
  535. }
  536. let ps = ScheduledNightscoutProfile(
  537. dia: settingsManager.pumpSettings.insulinActionCurve,
  538. carbs_hr: Int(carbs_hr),
  539. delay: 0,
  540. timezone: TimeZone.current.identifier,
  541. target_low: target_low,
  542. target_high: target_high,
  543. sens: sens,
  544. basal: basal,
  545. carbratio: cr,
  546. units: nsUnits
  547. )
  548. let defaultProfile = "default"
  549. let now = Date()
  550. let p = NightscoutProfileStore(
  551. defaultProfile: defaultProfile,
  552. startDate: now,
  553. mills: Int(now.timeIntervalSince1970) * 1000,
  554. units: nsUnits,
  555. enteredBy: NightscoutTreatment.local,
  556. store: [defaultProfile: ps]
  557. )
  558. guard let nightscout = nightscoutAPI, isNetworkReachable, isUploadEnabled else {
  559. return
  560. }
  561. // UPLOAD PREFERNCES WHEN CHANGED
  562. if let uploadedPreferences = storage.retrieve(OpenAPS.Nightscout.uploadedPreferences, as: Preferences.self),
  563. uploadedPreferences.rawJSON.sorted() == preferences.rawJSON.sorted(), !force
  564. {
  565. NSLog("NightscoutManager Preferences, preferences unchanged")
  566. } else { uploadPreferences(preferences) }
  567. // UPLOAD FreeAPS Settings WHEN CHANGED
  568. if let uploadedSettings = storage.retrieve(OpenAPS.Nightscout.uploadedSettings, as: FreeAPSSettings.self),
  569. uploadedSettings.rawJSON.sorted() == settings.rawJSON.sorted(), !force
  570. {
  571. NSLog("NightscoutManager Settings, settings unchanged")
  572. } else { uploadSettings(settings) }
  573. // UPLOAD Profiles WHEN CHANGED
  574. if let uploadedProfile = storage.retrieve(OpenAPS.Nightscout.uploadedProfile, as: NightscoutProfileStore.self),
  575. (uploadedProfile.store["default"]?.rawJSON ?? "").sorted() == ps.rawJSON.sorted(), !force
  576. {
  577. NSLog("NightscoutManager uploadProfile, no profile change")
  578. } else {
  579. processQueue.async {
  580. nightscout.uploadProfile(p)
  581. .sink { completion in
  582. switch completion {
  583. case .finished:
  584. self.storage.save(p, as: OpenAPS.Nightscout.uploadedProfile)
  585. debug(.nightscout, "Profile uploaded")
  586. case let .failure(error):
  587. debug(.nightscout, error.localizedDescription)
  588. }
  589. } receiveValue: {}
  590. .store(in: &self.lifetime)
  591. }
  592. }
  593. }
  594. func uploadGlucose() async {
  595. await uploadGlucose(glucoseStorage.getGlucoseNotYetUploadedToNightscout())
  596. await uploadTreatments(
  597. glucoseStorage.getCGMStateNotYetUploadedToNightscout(),
  598. fileToSave: OpenAPS.Nightscout.uploadedCGMState
  599. )
  600. }
  601. func uploadManualGlucose() async {
  602. await uploadManualGlucose(glucoseStorage.getManualGlucoseNotYetUploadedToNightscout())
  603. }
  604. private func uploadPumpHistory() async {
  605. await uploadTreatments(
  606. pumpHistoryStorage.getPumpHistoryNotYetUploadedToNightscout(),
  607. fileToSave: OpenAPS.Nightscout.uploadedPumphistory
  608. )
  609. }
  610. private func uploadCarbs() async {
  611. await uploadCarbs(carbsStorage.getCarbsNotYetUploadedToNightscout())
  612. await uploadCarbs(carbsStorage.getFPUsNotYetUploadedToNightscout())
  613. }
  614. private func uploadOverrides() async {
  615. await uploadOverrides(overridesStorage.getOverridesNotYetUploadedToNightscout())
  616. await uploadOverrideRuns(overridesStorage.getOverrideRunsNotYetUploadedToNightscout())
  617. }
  618. private func uploadTempTargets() async {
  619. await uploadTreatments(
  620. tempTargetsStorage.nightscoutTreatmentsNotUploaded(),
  621. fileToSave: OpenAPS.Nightscout.uploadedTempTargets
  622. )
  623. }
  624. private func uploadGlucose(_ glucose: [BloodGlucose]) async {
  625. guard !glucose.isEmpty, let nightscout = nightscoutAPI, isUploadEnabled, isUploadGlucoseEnabled else {
  626. return
  627. }
  628. do {
  629. // Upload in Batches of 100
  630. for chunk in glucose.chunks(ofCount: 100) {
  631. try await nightscout.uploadGlucose(Array(chunk))
  632. }
  633. // If successful, update the isUploadedToNS property of the GlucoseStored objects
  634. await updateGlucoseAsUploaded(glucose)
  635. debug(.nightscout, "Glucose uploaded")
  636. } catch {
  637. debug(.nightscout, "Upload of glucose failed: \(error.localizedDescription)")
  638. }
  639. }
  640. private func updateGlucoseAsUploaded(_ glucose: [BloodGlucose]) async {
  641. await backgroundContext.perform {
  642. let ids = glucose.map(\.id) as NSArray
  643. print("\(DebuggingIdentifiers.inProgress) ids: \(ids)")
  644. let fetchRequest: NSFetchRequest<GlucoseStored> = GlucoseStored.fetchRequest()
  645. fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
  646. do {
  647. let results = try self.backgroundContext.fetch(fetchRequest)
  648. print("\(DebuggingIdentifiers.inProgress) results: \(results)")
  649. for result in results {
  650. result.isUploadedToNS = true
  651. }
  652. guard self.backgroundContext.hasChanges else { return }
  653. try self.backgroundContext.save()
  654. } catch let error as NSError {
  655. debugPrint(
  656. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToNS: \(error.userInfo)"
  657. )
  658. }
  659. }
  660. }
  661. private func uploadTreatments(_ treatments: [NightscoutTreatment], fileToSave _: String) async {
  662. guard !treatments.isEmpty, let nightscout = nightscoutAPI, isUploadEnabled else {
  663. return
  664. }
  665. do {
  666. for chunk in treatments.chunks(ofCount: 100) {
  667. try await nightscout.uploadTreatments(Array(chunk))
  668. }
  669. // If successful, update the isUploadedToNS property of the PumpEventStored objects
  670. await updateTreatmentsAsUploaded(treatments)
  671. debug(.nightscout, "Treatments uploaded")
  672. } catch {
  673. debug(.nightscout, error.localizedDescription)
  674. }
  675. }
  676. private func updateTreatmentsAsUploaded(_ treatments: [NightscoutTreatment]) async {
  677. await backgroundContext.perform {
  678. let ids = treatments.map(\.id) as NSArray
  679. print("\(DebuggingIdentifiers.inProgress) ids: \(ids)")
  680. let fetchRequest: NSFetchRequest<PumpEventStored> = PumpEventStored.fetchRequest()
  681. fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
  682. do {
  683. let results = try self.backgroundContext.fetch(fetchRequest)
  684. print("\(DebuggingIdentifiers.inProgress) results: \(results)")
  685. for result in results {
  686. result.isUploadedToNS = true
  687. }
  688. guard self.backgroundContext.hasChanges else { return }
  689. try self.backgroundContext.save()
  690. } catch let error as NSError {
  691. debugPrint(
  692. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToNS: \(error.userInfo)"
  693. )
  694. }
  695. }
  696. }
  697. private func uploadManualGlucose(_ treatments: [NightscoutTreatment]) async {
  698. guard !treatments.isEmpty, let nightscout = nightscoutAPI, isUploadEnabled else {
  699. return
  700. }
  701. do {
  702. for chunk in treatments.chunks(ofCount: 100) {
  703. try await nightscout.uploadTreatments(Array(chunk))
  704. }
  705. // If successful, update the isUploadedToNS property of the GlucoseStored objects
  706. await updateManualGlucoseAsUploaded(treatments)
  707. debug(.nightscout, "Treatments uploaded")
  708. } catch {
  709. debug(.nightscout, error.localizedDescription)
  710. }
  711. }
  712. private func updateManualGlucoseAsUploaded(_ treatments: [NightscoutTreatment]) async {
  713. await backgroundContext.perform {
  714. let ids = treatments.map(\.id) as NSArray
  715. print("\(DebuggingIdentifiers.inProgress) ids: \(ids)")
  716. let fetchRequest: NSFetchRequest<GlucoseStored> = GlucoseStored.fetchRequest()
  717. fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
  718. do {
  719. let results = try self.backgroundContext.fetch(fetchRequest)
  720. print("\(DebuggingIdentifiers.inProgress) results: \(results)")
  721. for result in results {
  722. result.isUploadedToNS = true
  723. }
  724. guard self.backgroundContext.hasChanges else { return }
  725. try self.backgroundContext.save()
  726. } catch let error as NSError {
  727. debugPrint(
  728. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToNS: \(error.userInfo)"
  729. )
  730. }
  731. }
  732. }
  733. private func uploadCarbs(_ treatments: [NightscoutTreatment]) async {
  734. guard !treatments.isEmpty, let nightscout = nightscoutAPI, isUploadEnabled else {
  735. return
  736. }
  737. do {
  738. for chunk in treatments.chunks(ofCount: 100) {
  739. try await nightscout.uploadTreatments(Array(chunk))
  740. }
  741. // If successful, update the isUploadedToNS property of the CarbEntryStored objects
  742. await updateCarbsAsUploaded(treatments)
  743. debug(.nightscout, "Treatments uploaded")
  744. } catch {
  745. debug(.nightscout, error.localizedDescription)
  746. }
  747. }
  748. private func updateCarbsAsUploaded(_ treatments: [NightscoutTreatment]) async {
  749. await backgroundContext.perform {
  750. let ids = treatments.map(\.id) as NSArray
  751. print("\(DebuggingIdentifiers.inProgress) ids: \(ids)")
  752. let fetchRequest: NSFetchRequest<CarbEntryStored> = CarbEntryStored.fetchRequest()
  753. fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
  754. do {
  755. let results = try self.backgroundContext.fetch(fetchRequest)
  756. print("\(DebuggingIdentifiers.inProgress) results: \(results)")
  757. for result in results {
  758. result.isUploadedToNS = true
  759. }
  760. guard self.backgroundContext.hasChanges else { return }
  761. try self.backgroundContext.save()
  762. } catch let error as NSError {
  763. debugPrint(
  764. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToNS: \(error.userInfo)"
  765. )
  766. }
  767. }
  768. }
  769. private func uploadOverrides(_ overrides: [NightscoutExercise]) async {
  770. guard !overrides.isEmpty, let nightscout = nightscoutAPI, isUploadEnabled else {
  771. return
  772. }
  773. do {
  774. for chunk in overrides.chunks(ofCount: 100) {
  775. try await nightscout.uploadOverrides(Array(chunk))
  776. }
  777. // If successful, update the isUploadedToNS property of the OverrideStored objects
  778. await updateOverridesAsUploaded(overrides)
  779. debug(.nightscout, "Overrides uploaded")
  780. } catch {
  781. debug(.nightscout, error.localizedDescription)
  782. }
  783. }
  784. private func updateOverridesAsUploaded(_ overrides: [NightscoutExercise]) async {
  785. await backgroundContext.perform {
  786. let ids = overrides.map(\.id) as NSArray
  787. print("\(DebuggingIdentifiers.inProgress) ids: \(ids)")
  788. let fetchRequest: NSFetchRequest<OverrideStored> = OverrideStored.fetchRequest()
  789. fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
  790. do {
  791. let results = try self.backgroundContext.fetch(fetchRequest)
  792. print("\(DebuggingIdentifiers.inProgress) results: \(results)")
  793. for result in results {
  794. result.isUploadedToNS = true
  795. }
  796. guard self.backgroundContext.hasChanges else { return }
  797. try self.backgroundContext.save()
  798. } catch let error as NSError {
  799. debugPrint(
  800. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToNS: \(error.userInfo)"
  801. )
  802. }
  803. }
  804. }
  805. private func uploadOverrideRuns(_ overrideRuns: [NightscoutExercise]) async {
  806. guard !overrideRuns.isEmpty, let nightscout = nightscoutAPI, isUploadEnabled else {
  807. return
  808. }
  809. do {
  810. for chunk in overrideRuns.chunks(ofCount: 100) {
  811. try await nightscout.uploadOverrides(Array(chunk))
  812. }
  813. // If successful, update the isUploadedToNS property of the OverrideRunStored objects
  814. await updateOverrideRunsAsUploaded(overrideRuns)
  815. debug(.nightscout, "Overrides uploaded")
  816. } catch {
  817. debug(.nightscout, error.localizedDescription)
  818. }
  819. }
  820. private func updateOverrideRunsAsUploaded(_ overrideRuns: [NightscoutExercise]) async {
  821. await backgroundContext.perform {
  822. let ids = overrideRuns.map(\.id) as NSArray
  823. print("\(DebuggingIdentifiers.inProgress) ids: \(ids)")
  824. let fetchRequest: NSFetchRequest<OverrideRunStored> = OverrideRunStored.fetchRequest()
  825. fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
  826. do {
  827. let results = try self.backgroundContext.fetch(fetchRequest)
  828. print("\(DebuggingIdentifiers.inProgress) results: \(results)")
  829. for result in results {
  830. result.isUploadedToNS = true
  831. }
  832. guard self.backgroundContext.hasChanges else { return }
  833. try self.backgroundContext.save()
  834. } catch let error as NSError {
  835. debugPrint(
  836. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToNS: \(error.userInfo)"
  837. )
  838. }
  839. }
  840. }
  841. }
  842. extension Array {
  843. func chunks(ofCount count: Int) -> [[Element]] {
  844. stride(from: 0, to: self.count, by: count).map {
  845. Array(self[$0 ..< Swift.min($0 + count, self.count)])
  846. }
  847. }
  848. }