NightscoutManager.swift 37 KB

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