APSManager.swift 57 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449
  1. import Combine
  2. import CoreData
  3. import Foundation
  4. import LoopKit
  5. import LoopKitUI
  6. import SwiftDate
  7. import Swinject
  8. protocol APSManager {
  9. func heartbeat(date: Date)
  10. func enactBolus(amount: Double, isSMB: Bool) async
  11. var pumpManager: PumpManagerUI? { get set }
  12. var bluetoothManager: BluetoothStateManager? { get }
  13. var pumpDisplayState: CurrentValueSubject<PumpDisplayState?, Never> { get }
  14. var pumpName: CurrentValueSubject<String, Never> { get }
  15. var isLooping: CurrentValueSubject<Bool, Never> { get }
  16. var lastLoopDate: Date { get }
  17. var lastLoopDateSubject: PassthroughSubject<Date, Never> { get }
  18. var bolusProgress: CurrentValueSubject<Decimal?, Never> { get }
  19. var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> { get }
  20. var isManualTempBasal: Bool { get }
  21. func enactTempBasal(rate: Double, duration: TimeInterval) async
  22. func determineBasal() async -> Bool
  23. func determineBasalSync() async
  24. func simulateDetermineBasal(carbs: Decimal, iob: Decimal) async -> Determination?
  25. func roundBolus(amount: Decimal) -> Decimal
  26. var lastError: CurrentValueSubject<Error?, Never> { get }
  27. func cancelBolus() async
  28. }
  29. enum APSError: LocalizedError {
  30. case pumpError(Error)
  31. case invalidPumpState(message: String)
  32. case glucoseError(message: String)
  33. case apsError(message: String)
  34. case deviceSyncError(message: String)
  35. case manualBasalTemp(message: String)
  36. var errorDescription: String? {
  37. switch self {
  38. case let .pumpError(error):
  39. return "Pump error: \(error.localizedDescription)"
  40. case let .invalidPumpState(message):
  41. return "Error: Invalid Pump State: \(message)"
  42. case let .glucoseError(message):
  43. return "Error: Invalid glucose: \(message)"
  44. case let .apsError(message):
  45. return "APS error: \(message)"
  46. case let .deviceSyncError(message):
  47. return "Sync error: \(message)"
  48. case let .manualBasalTemp(message):
  49. return "Manual Basal Temp : \(message)"
  50. }
  51. }
  52. }
  53. final class BaseAPSManager: APSManager, Injectable {
  54. private let processQueue = DispatchQueue(label: "BaseAPSManager.processQueue")
  55. @Injected() private var storage: FileStorage!
  56. @Injected() private var pumpHistoryStorage: PumpHistoryStorage!
  57. @Injected() private var alertHistoryStorage: AlertHistoryStorage!
  58. @Injected() private var tempTargetsStorage: TempTargetsStorage!
  59. @Injected() private var carbsStorage: CarbsStorage!
  60. @Injected() private var determinationStorage: DeterminationStorage!
  61. @Injected() private var deviceDataManager: DeviceDataManager!
  62. @Injected() private var nightscout: NightscoutManager!
  63. @Injected() private var settingsManager: SettingsManager!
  64. @Injected() private var tddStorage: TDDStorage!
  65. @Injected() private var broadcaster: Broadcaster!
  66. @Persisted(key: "lastLoopStartDate") private var lastLoopStartDate: Date = .distantPast
  67. @Persisted(key: "lastLoopDate") var lastLoopDate: Date = .distantPast {
  68. didSet {
  69. lastLoopDateSubject.send(lastLoopDate)
  70. }
  71. }
  72. let viewContext = CoreDataStack.shared.persistentContainer.viewContext
  73. let privateContext = CoreDataStack.shared.newTaskContext()
  74. private var openAPS: OpenAPS!
  75. private var lifetime = Lifetime()
  76. private var backGroundTaskID: UIBackgroundTaskIdentifier?
  77. var pumpManager: PumpManagerUI? {
  78. get { deviceDataManager.pumpManager }
  79. set { deviceDataManager.pumpManager = newValue }
  80. }
  81. var bluetoothManager: BluetoothStateManager? { deviceDataManager.bluetoothManager }
  82. @Persisted(key: "isManualTempBasal") var isManualTempBasal: Bool = false
  83. let isLooping = CurrentValueSubject<Bool, Never>(false)
  84. let lastLoopDateSubject = PassthroughSubject<Date, Never>()
  85. let lastError = CurrentValueSubject<Error?, Never>(nil)
  86. let bolusProgress = CurrentValueSubject<Decimal?, Never>(nil)
  87. var pumpDisplayState: CurrentValueSubject<PumpDisplayState?, Never> {
  88. deviceDataManager.pumpDisplayState
  89. }
  90. var pumpName: CurrentValueSubject<String, Never> {
  91. deviceDataManager.pumpName
  92. }
  93. var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> {
  94. deviceDataManager.pumpExpiresAtDate
  95. }
  96. var settings: FreeAPSSettings {
  97. get { settingsManager.settings }
  98. set { settingsManager.settings = newValue }
  99. }
  100. init(resolver: Resolver) {
  101. injectServices(resolver)
  102. openAPS = OpenAPS(storage: storage)
  103. subscribe()
  104. lastLoopDateSubject.send(lastLoopDate)
  105. isLooping
  106. .weakAssign(to: \.deviceDataManager.loopInProgress, on: self)
  107. .store(in: &lifetime)
  108. }
  109. private func subscribe() {
  110. if settingsManager.settings.units == .mmolL {
  111. let wasParsed = storage.parseOnFileSettingsToMgdL()
  112. if wasParsed {
  113. Task {
  114. await openAPS.createProfiles()
  115. }
  116. }
  117. }
  118. deviceDataManager.recommendsLoop
  119. .receive(on: processQueue)
  120. .sink { [weak self] in
  121. self?.loop()
  122. }
  123. .store(in: &lifetime)
  124. pumpManager?.addStatusObserver(self, queue: processQueue)
  125. deviceDataManager.errorSubject
  126. .receive(on: processQueue)
  127. .map { APSError.pumpError($0) }
  128. .sink {
  129. self.processError($0)
  130. }
  131. .store(in: &lifetime)
  132. deviceDataManager.bolusTrigger
  133. .receive(on: processQueue)
  134. .sink { bolusing in
  135. if bolusing {
  136. self.createBolusReporter()
  137. } else {
  138. self.clearBolusReporter()
  139. }
  140. }
  141. .store(in: &lifetime)
  142. // manage a manual Temp Basal from OmniPod - Force loop() after stop a temp basal or finished
  143. deviceDataManager.manualTempBasal
  144. .receive(on: processQueue)
  145. .sink { manualBasal in
  146. if manualBasal {
  147. self.isManualTempBasal = true
  148. } else {
  149. if self.isManualTempBasal {
  150. self.isManualTempBasal = false
  151. self.loop()
  152. }
  153. }
  154. }
  155. .store(in: &lifetime)
  156. }
  157. func heartbeat(date: Date) {
  158. deviceDataManager.heartbeat(date: date)
  159. }
  160. // Loop entry point
  161. private func loop() {
  162. Task {
  163. // check the last start of looping is more the loopInterval but the previous loop was completed
  164. if lastLoopDate > lastLoopStartDate {
  165. guard lastLoopStartDate.addingTimeInterval(Config.loopInterval) < Date() else {
  166. debug(.apsManager, "too close to do a loop : \(lastLoopStartDate)")
  167. return
  168. }
  169. }
  170. guard !isLooping.value else {
  171. warning(.apsManager, "Loop already in progress. Skip recommendation.")
  172. return
  173. }
  174. // start background time extension
  175. backGroundTaskID = await UIApplication.shared.beginBackgroundTask(withName: "Loop starting") {
  176. guard let backgroundTask = self.backGroundTaskID else { return }
  177. Task {
  178. UIApplication.shared.endBackgroundTask(backgroundTask)
  179. }
  180. self.backGroundTaskID = .invalid
  181. }
  182. lastLoopStartDate = Date()
  183. var previousLoop = [LoopStatRecord]()
  184. var interval: Double?
  185. do {
  186. try await privateContext.perform {
  187. let requestStats = LoopStatRecord.fetchRequest() as NSFetchRequest<LoopStatRecord>
  188. let sortStats = NSSortDescriptor(key: "end", ascending: false)
  189. requestStats.sortDescriptors = [sortStats]
  190. requestStats.fetchLimit = 1
  191. previousLoop = try self.privateContext.fetch(requestStats)
  192. if (previousLoop.first?.end ?? .distantFuture) < self.lastLoopStartDate {
  193. interval = self.roundDouble(
  194. (self.lastLoopStartDate - (previousLoop.first?.end ?? Date())).timeInterval / 60,
  195. 1
  196. )
  197. }
  198. }
  199. } catch let error as NSError {
  200. debugPrint(
  201. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch the last loop with error: \(error.userInfo)"
  202. )
  203. }
  204. var loopStatRecord = LoopStats(
  205. start: lastLoopStartDate,
  206. loopStatus: "Starting",
  207. interval: interval
  208. )
  209. isLooping.send(true)
  210. do {
  211. if await !determineBasal() {
  212. throw APSError.apsError(message: "Determine basal failed")
  213. }
  214. // Open loop completed
  215. guard settings.closedLoop else {
  216. loopStatRecord.end = Date()
  217. loopStatRecord.duration = roundDouble((loopStatRecord.end! - loopStatRecord.start).timeInterval / 60, 2)
  218. loopStatRecord.loopStatus = "Success"
  219. await loopCompleted(loopStatRecord: loopStatRecord)
  220. return
  221. }
  222. // Closed loop - enact Determination
  223. try await enactDetermination()
  224. loopStatRecord.end = Date()
  225. loopStatRecord.duration = roundDouble((loopStatRecord.end! - loopStatRecord.start).timeInterval / 60, 2)
  226. loopStatRecord.loopStatus = "Success"
  227. await loopCompleted(loopStatRecord: loopStatRecord)
  228. } catch {
  229. loopStatRecord.end = Date()
  230. loopStatRecord.duration = roundDouble((loopStatRecord.end! - loopStatRecord.start).timeInterval / 60, 2)
  231. loopStatRecord.loopStatus = error.localizedDescription
  232. await loopCompleted(error: error, loopStatRecord: loopStatRecord)
  233. }
  234. if let nightscoutManager = nightscout {
  235. await nightscoutManager.uploadCarbs()
  236. await nightscoutManager.uploadPumpHistory()
  237. await nightscoutManager.uploadOverrides()
  238. await nightscoutManager.uploadTempTargets()
  239. }
  240. // End background task after all the operations are completed
  241. if let backgroundTask = self.backGroundTaskID {
  242. await UIApplication.shared.endBackgroundTask(backgroundTask)
  243. self.backGroundTaskID = .invalid
  244. }
  245. }
  246. }
  247. // Loop exit point
  248. private func loopCompleted(error: Error? = nil, loopStatRecord: LoopStats) async {
  249. isLooping.send(false)
  250. if let error = error {
  251. warning(.apsManager, "Loop failed with error: \(error.localizedDescription)")
  252. if let backgroundTask = backGroundTaskID {
  253. await UIApplication.shared.endBackgroundTask(backgroundTask)
  254. backGroundTaskID = .invalid
  255. }
  256. processError(error)
  257. } else {
  258. debug(.apsManager, "Loop succeeded")
  259. lastLoopDate = Date()
  260. lastError.send(nil)
  261. }
  262. loopStats(loopStatRecord: loopStatRecord)
  263. if settings.closedLoop {
  264. await reportEnacted(wasEnacted: error == nil)
  265. }
  266. // End of the BG tasks
  267. if let backgroundTask = backGroundTaskID {
  268. await UIApplication.shared.endBackgroundTask(backgroundTask)
  269. backGroundTaskID = .invalid
  270. }
  271. }
  272. private func verifyStatus() -> Error? {
  273. guard let pump = pumpManager else {
  274. return APSError.invalidPumpState(message: "Pump not set")
  275. }
  276. let status = pump.status.pumpStatus
  277. guard !status.bolusing else {
  278. return APSError.invalidPumpState(message: "Pump is bolusing")
  279. }
  280. guard !status.suspended else {
  281. return APSError.invalidPumpState(message: "Pump suspended")
  282. }
  283. let reservoir = storage.retrieve(OpenAPS.Monitor.reservoir, as: Decimal.self) ?? 100
  284. guard reservoir >= 0 else {
  285. return APSError.invalidPumpState(message: "Reservoir is empty")
  286. }
  287. return nil
  288. }
  289. func autosense() async throws -> Bool {
  290. guard let autosense = await storage.retrieveAsync(OpenAPS.Settings.autosense, as: Autosens.self),
  291. (autosense.timestamp ?? .distantPast).addingTimeInterval(30.minutes.timeInterval) > Date()
  292. else {
  293. let result = try await openAPS.autosense()
  294. return result != nil
  295. }
  296. return false
  297. }
  298. /// Calculates and stores the Total Daily Dose (TDD)
  299. private func calculateAndStoreTDD() async {
  300. guard let pumpManager else { return }
  301. async let pumpHistory = pumpHistoryStorage.getPumpHistory()
  302. async let basalProfile = storage
  303. .retrieveAsync(OpenAPS.Settings.basalProfile, as: [BasalProfileEntry].self) ??
  304. [BasalProfileEntry](from: OpenAPS.defaults(for: OpenAPS.Settings.basalProfile)) ??
  305. [] // OpenAPS.defaults ensures we at least get default rate of 1u/hr for 24 hrs
  306. // Calculate TDD
  307. let tddResult = await tddStorage.calculateTDD(
  308. pumpManager: pumpManager,
  309. pumpHistory: await pumpHistory,
  310. basalProfile: await basalProfile
  311. )
  312. // Store TDD in Core Data
  313. await tddStorage.storeTDD(tddResult)
  314. }
  315. func determineBasal() async -> Bool {
  316. debug(.apsManager, "Start determine basal")
  317. await calculateAndStoreTDD()
  318. // Fetch glucose asynchronously
  319. let glucose = await fetchGlucose(predicate: NSPredicate.predicateForOneHourAgo, fetchLimit: 6)
  320. // Perform the context-related checks and actions
  321. let isValidGlucoseData = await privateContext.perform {
  322. guard glucose.count > 2 else {
  323. debug(.apsManager, "Not enough glucose data")
  324. self.processError(APSError.glucoseError(message: "Not enough glucose data"))
  325. return false
  326. }
  327. let dateOfLastGlucose = glucose.first?.date
  328. guard dateOfLastGlucose ?? Date() >= Date().addingTimeInterval(-12.minutes.timeInterval) else {
  329. debug(.apsManager, "Glucose data is stale")
  330. self.processError(APSError.glucoseError(message: "Glucose data is stale"))
  331. return false
  332. }
  333. guard !GlucoseStored.glucoseIsFlat(glucose) else {
  334. debug(.apsManager, "Glucose data is too flat")
  335. self.processError(APSError.glucoseError(message: "Glucose data is too flat"))
  336. return false
  337. }
  338. return true
  339. }
  340. guard isValidGlucoseData else {
  341. debug(.apsManager, "Glucose validation failed")
  342. processError(APSError.glucoseError(message: "Glucose validation failed"))
  343. return false
  344. }
  345. do {
  346. let now = Date()
  347. // Parallelize the fetches using async let
  348. async let currentTemp = fetchCurrentTempBasal(date: now)
  349. async let autosenseResult = autosense()
  350. _ = try await autosenseResult
  351. await openAPS.createProfiles()
  352. let determination = try await openAPS.determineBasal(currentTemp: await currentTemp, clock: now)
  353. if let determination = determination {
  354. DispatchQueue.main.async {
  355. self.broadcaster.notify(DeterminationObserver.self, on: .main) {
  356. $0.determinationDidUpdate(determination)
  357. }
  358. }
  359. return true
  360. } else {
  361. return false
  362. }
  363. } catch {
  364. debug(.apsManager, "Error determining basal: \(error)")
  365. return false
  366. }
  367. }
  368. func determineBasalSync() async {
  369. _ = await determineBasal()
  370. }
  371. func simulateDetermineBasal(carbs: Decimal, iob: Decimal) async -> Determination? {
  372. do {
  373. let temp = await fetchCurrentTempBasal(date: Date.now)
  374. return try await openAPS.determineBasal(currentTemp: temp, clock: Date(), carbs: carbs, iob: iob, simulation: true)
  375. } catch {
  376. debugPrint(
  377. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Error occurred in invokeDummyDetermineBasalSync: \(error)"
  378. )
  379. return nil
  380. }
  381. }
  382. func roundBolus(amount: Decimal) -> Decimal {
  383. guard let pump = pumpManager else { return amount }
  384. let rounded = Decimal(pump.roundToSupportedBolusVolume(units: Double(amount)))
  385. let maxBolus = Decimal(pump.roundToSupportedBolusVolume(units: Double(settingsManager.pumpSettings.maxBolus)))
  386. return min(rounded, maxBolus)
  387. }
  388. private var bolusReporter: DoseProgressReporter?
  389. func enactBolus(amount: Double, isSMB: Bool) async {
  390. if amount <= 0 {
  391. return
  392. }
  393. if let error = verifyStatus() {
  394. processError(error)
  395. processQueue.async {
  396. self.broadcaster.notify(BolusFailureObserver.self, on: self.processQueue) {
  397. $0.bolusDidFail()
  398. }
  399. }
  400. return
  401. }
  402. guard let pump = pumpManager else { return }
  403. let roundedAmount = pump.roundToSupportedBolusVolume(units: amount)
  404. debug(.apsManager, "Enact bolus \(roundedAmount), manual \(!isSMB)")
  405. do {
  406. try await pump.enactBolus(units: roundedAmount, automatic: isSMB)
  407. debug(.apsManager, "Bolus succeeded")
  408. if !isSMB {
  409. await determineBasalSync()
  410. }
  411. bolusProgress.send(0)
  412. } catch {
  413. warning(.apsManager, "Bolus failed with error: \(error.localizedDescription)")
  414. processError(APSError.pumpError(error))
  415. if !isSMB {
  416. processQueue.async {
  417. self.broadcaster.notify(BolusFailureObserver.self, on: self.processQueue) {
  418. $0.bolusDidFail()
  419. }
  420. }
  421. }
  422. }
  423. }
  424. func cancelBolus() async {
  425. guard let pump = pumpManager, pump.status.pumpStatus.bolusing else { return }
  426. debug(.apsManager, "Cancel bolus")
  427. do {
  428. _ = try await pump.cancelBolus()
  429. debug(.apsManager, "Bolus cancelled")
  430. } catch {
  431. debug(.apsManager, "Bolus cancellation failed with error: \(error.localizedDescription)")
  432. processError(APSError.pumpError(error))
  433. }
  434. bolusReporter?.removeObserver(self)
  435. bolusReporter = nil
  436. bolusProgress.send(nil)
  437. }
  438. func enactTempBasal(rate: Double, duration: TimeInterval) async {
  439. if let error = verifyStatus() {
  440. processError(error)
  441. return
  442. }
  443. guard let pump = pumpManager else { return }
  444. // unable to do temp basal during manual temp basal 😁
  445. if isManualTempBasal {
  446. processError(APSError.manualBasalTemp(message: "Loop not possible during the manual basal temp"))
  447. return
  448. }
  449. debug(.apsManager, "Enact temp basal \(rate) - \(duration)")
  450. let roundedAmout = pump.roundToSupportedBasalRate(unitsPerHour: rate)
  451. do {
  452. try await pump.enactTempBasal(unitsPerHour: roundedAmout, for: duration)
  453. debug(.apsManager, "Temp Basal succeeded")
  454. } catch {
  455. debug(.apsManager, "Temp Basal failed with error: \(error.localizedDescription)")
  456. processError(APSError.pumpError(error))
  457. }
  458. }
  459. private func fetchCurrentTempBasal(date: Date) async -> TempBasal {
  460. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  461. ofType: PumpEventStored.self,
  462. onContext: privateContext,
  463. predicate: NSPredicate.recentPumpHistory,
  464. key: "timestamp",
  465. ascending: false,
  466. fetchLimit: 1
  467. )
  468. let fetchedTempBasal = await privateContext.perform {
  469. guard let fetchedResults = results as? [PumpEventStored],
  470. let tempBasalEvent = fetchedResults.first,
  471. let tempBasal = tempBasalEvent.tempBasal,
  472. let eventTimestamp = tempBasalEvent.timestamp
  473. else {
  474. return TempBasal(duration: 0, rate: 0, temp: .absolute, timestamp: date)
  475. }
  476. let delta = Int((date.timeIntervalSince1970 - eventTimestamp.timeIntervalSince1970) / 60)
  477. let duration = max(0, Int(tempBasal.duration) - delta)
  478. let rate = tempBasal.rate as? Decimal ?? 0
  479. return TempBasal(duration: duration, rate: rate, temp: .absolute, timestamp: date)
  480. }
  481. guard let state = pumpManager?.status.basalDeliveryState else { return fetchedTempBasal }
  482. switch state {
  483. case .active:
  484. return TempBasal(duration: 0, rate: 0, temp: .absolute, timestamp: date)
  485. case let .tempBasal(dose):
  486. let rate = Decimal(dose.unitsPerHour)
  487. let durationMin = max(0, Int((dose.endDate.timeIntervalSince1970 - date.timeIntervalSince1970) / 60))
  488. return TempBasal(duration: durationMin, rate: rate, temp: .absolute, timestamp: date)
  489. default:
  490. return fetchedTempBasal
  491. }
  492. }
  493. private func enactDetermination() async throws {
  494. guard let determinationID = await determinationStorage
  495. .fetchLastDeterminationObjectID(predicate: NSPredicate.predicateFor30MinAgoForDetermination).first
  496. else {
  497. throw APSError.apsError(message: "Determination not found")
  498. }
  499. guard let pump = pumpManager else {
  500. throw APSError.apsError(message: "Pump not set")
  501. }
  502. // Unable to do temp basal during manual temp basal 😁
  503. if isManualTempBasal {
  504. throw APSError.manualBasalTemp(message: "Loop not possible during the manual basal temp")
  505. }
  506. let (rateDecimal, durationInSeconds, smbToDeliver) = try await setValues(determinationID: determinationID)
  507. if let rate = rateDecimal, let duration = durationInSeconds {
  508. try await performBasal(pump: pump, rate: rate, duration: duration)
  509. }
  510. // only perform a bolus if smbToDeliver is > 0
  511. if let smb = smbToDeliver, smb.compare(NSDecimalNumber(value: 0)) == .orderedDescending {
  512. try await performBolus(pump: pump, smbToDeliver: smb)
  513. }
  514. }
  515. private func setValues(determinationID: NSManagedObjectID) async throws
  516. -> (NSDecimalNumber?, TimeInterval?, NSDecimalNumber?)
  517. {
  518. return try await privateContext.perform {
  519. do {
  520. let determination = try self.privateContext.existingObject(with: determinationID) as? OrefDetermination
  521. let rate = determination?.rate
  522. let duration = determination?.duration.flatMap { TimeInterval(truncating: $0) * 60 }
  523. let smbToDeliver = determination?.smbToDeliver ?? 0
  524. return (rate, duration, smbToDeliver)
  525. } catch {
  526. throw error
  527. }
  528. }
  529. }
  530. private func performBasal(pump: PumpManager, rate: NSDecimalNumber, duration: TimeInterval) async throws {
  531. try await pump.enactTempBasal(unitsPerHour: Double(truncating: rate), for: duration)
  532. }
  533. private func performBolus(pump: PumpManager, smbToDeliver: NSDecimalNumber) async throws {
  534. try await pump.enactBolus(units: Double(truncating: smbToDeliver), automatic: true)
  535. bolusProgress.send(0)
  536. }
  537. private func reportEnacted(wasEnacted: Bool) async {
  538. guard let determinationID = await determinationStorage
  539. .fetchLastDeterminationObjectID(predicate: NSPredicate.predicateFor30MinAgoForDetermination).first
  540. else {
  541. return
  542. }
  543. await privateContext.perform {
  544. if let determinationUpdated = self.privateContext.object(with: determinationID) as? OrefDetermination {
  545. determinationUpdated.timestamp = Date()
  546. determinationUpdated.enacted = wasEnacted
  547. determinationUpdated.isUploadedToNS = false
  548. do {
  549. guard self.privateContext.hasChanges else { return }
  550. try self.privateContext.save()
  551. debugPrint("Update successful in reportEnacted() \(DebuggingIdentifiers.succeeded)")
  552. } catch {
  553. debugPrint(
  554. "Failed \(DebuggingIdentifiers.succeeded) to save context in reportEnacted(): \(error.localizedDescription)"
  555. )
  556. }
  557. debug(.apsManager, "Determination enacted. Enacted: \(wasEnacted)")
  558. Task.detached(priority: .low) {
  559. await self.statistics()
  560. }
  561. } else {
  562. debugPrint("Failed to update OrefDetermination in reportEnacted()")
  563. }
  564. }
  565. }
  566. private func roundDecimal(_ decimal: Decimal, _ digits: Double) -> Decimal {
  567. let rounded = round(Double(decimal) * pow(10, digits)) / pow(10, digits)
  568. return Decimal(rounded)
  569. }
  570. private func roundDouble(_ double: Double, _ digits: Double) -> Double {
  571. let rounded = round(Double(double) * pow(10, digits)) / pow(10, digits)
  572. return rounded
  573. }
  574. private func medianCalculationDouble(array: [Double]) -> Double {
  575. guard !array.isEmpty else {
  576. return 0
  577. }
  578. let sorted = array.sorted()
  579. let length = array.count
  580. if length % 2 == 0 {
  581. return (sorted[length / 2 - 1] + sorted[length / 2]) / 2
  582. }
  583. return sorted[length / 2]
  584. }
  585. private func medianCalculation(array: [Int]) -> Double {
  586. guard !array.isEmpty else {
  587. return 0
  588. }
  589. let sorted = array.sorted()
  590. let length = array.count
  591. if length % 2 == 0 {
  592. return Double((sorted[length / 2 - 1] + sorted[length / 2]) / 2)
  593. }
  594. return Double(sorted[length / 2])
  595. }
  596. private func tir(_ glucose: [GlucoseStored]) -> (TIR: Double, hypos: Double, hypers: Double, normal_: Double) {
  597. privateContext.perform {
  598. let justGlucoseArray = glucose.compactMap({ each in Int(each.glucose as Int16) })
  599. let totalReadings = justGlucoseArray.count
  600. let highLimit = settingsManager.settings.high
  601. let lowLimit = settingsManager.settings.low
  602. let hyperArray = glucose.filter({ $0.glucose >= Int(highLimit) })
  603. let hyperReadings = hyperArray.compactMap({ each in each.glucose as Int16 }).count
  604. let hyperPercentage = Double(hyperReadings) / Double(totalReadings) * 100
  605. let hypoArray = glucose.filter({ $0.glucose <= Int(lowLimit) })
  606. let hypoReadings = hypoArray.compactMap({ each in each.glucose as Int16 }).count
  607. let hypoPercentage = Double(hypoReadings) / Double(totalReadings) * 100
  608. // Euglyccemic range
  609. let normalArray = glucose.filter({ $0.glucose >= 70 && $0.glucose <= 140 })
  610. let normalReadings = normalArray.compactMap({ each in each.glucose as Int16 }).count
  611. let normalPercentage = Double(normalReadings) / Double(totalReadings) * 100
  612. // TIR
  613. let tir = 100 - (hypoPercentage + hyperPercentage)
  614. return (
  615. roundDouble(tir, 1),
  616. roundDouble(hypoPercentage, 1),
  617. roundDouble(hyperPercentage, 1),
  618. roundDouble(normalPercentage, 1)
  619. )
  620. }
  621. }
  622. private func glucoseStats(_ fetchedGlucose: [GlucoseStored])
  623. -> (ifcc: Double, ngsp: Double, average: Double, median: Double, sd: Double, cv: Double, readings: Double)
  624. {
  625. let glucose = fetchedGlucose
  626. // First date
  627. let last = glucose.last?.date ?? Date()
  628. // Last date (recent)
  629. let first = glucose.first?.date ?? Date()
  630. // Total time in days
  631. let numberOfDays = (first - last).timeInterval / 8.64E4
  632. let denominator = numberOfDays < 1 ? 1 : numberOfDays
  633. let justGlucoseArray = glucose.compactMap({ each in Int(each.glucose as Int16) })
  634. let sumReadings = justGlucoseArray.reduce(0, +)
  635. let countReadings = justGlucoseArray.count
  636. let glucoseAverage = Double(sumReadings) / Double(countReadings)
  637. let medianGlucose = medianCalculation(array: justGlucoseArray)
  638. var NGSPa1CStatisticValue = 0.0
  639. var IFCCa1CStatisticValue = 0.0
  640. NGSPa1CStatisticValue = (glucoseAverage + 46.7) / 28.7 // NGSP (%)
  641. IFCCa1CStatisticValue = 10.929 *
  642. (NGSPa1CStatisticValue - 2.152) // IFCC (mmol/mol) A1C(mmol/mol) = 10.929 * (A1C(%) - 2.15)
  643. var sumOfSquares = 0.0
  644. for array in justGlucoseArray {
  645. sumOfSquares += pow(Double(array) - Double(glucoseAverage), 2)
  646. }
  647. var sd = 0.0
  648. var cv = 0.0
  649. // Avoid division by zero
  650. if glucoseAverage > 0 {
  651. sd = sqrt(sumOfSquares / Double(countReadings))
  652. cv = sd / Double(glucoseAverage) * 100
  653. }
  654. let conversionFactor = 0.0555
  655. let units = settingsManager.settings.units
  656. var output: (ifcc: Double, ngsp: Double, average: Double, median: Double, sd: Double, cv: Double, readings: Double)
  657. output = (
  658. ifcc: IFCCa1CStatisticValue,
  659. ngsp: NGSPa1CStatisticValue,
  660. average: glucoseAverage * (units == .mmolL ? conversionFactor : 1),
  661. median: medianGlucose * (units == .mmolL ? conversionFactor : 1),
  662. sd: sd * (units == .mmolL ? conversionFactor : 1), cv: cv,
  663. readings: Double(countReadings) / denominator
  664. )
  665. return output
  666. }
  667. private func loops(_ fetchedLoops: [LoopStatRecord]) -> Loops {
  668. let loops = fetchedLoops
  669. // First date
  670. let previous = loops.last?.end ?? Date()
  671. // Last date (recent)
  672. let current = loops.first?.start ?? Date()
  673. // Total time in days
  674. let totalTime = (current - previous).timeInterval / 8.64E4
  675. //
  676. let durationArray = loops.compactMap({ each in each.duration })
  677. let durationArrayCount = durationArray.count
  678. let durationAverage = durationArray.reduce(0, +) / Double(durationArrayCount) * 60
  679. let medianDuration = medianCalculationDouble(array: durationArray) * 60
  680. let max_duration = (durationArray.max() ?? 0) * 60
  681. let min_duration = (durationArray.min() ?? 0) * 60
  682. let successsNR = loops.compactMap({ each in each.loopStatus }).filter({ each in each!.contains("Success") }).count
  683. let errorNR = durationArrayCount - successsNR
  684. let total = Double(successsNR + errorNR) == 0 ? 1 : Double(successsNR + errorNR)
  685. let successRate: Double? = (Double(successsNR) / total) * 100
  686. let loopNr = totalTime <= 1 ? total : round(total / (totalTime != 0 ? totalTime : 1))
  687. let intervalArray = loops.compactMap({ each in each.interval as Double })
  688. let count = intervalArray.count != 0 ? intervalArray.count : 1
  689. let median_interval = medianCalculationDouble(array: intervalArray)
  690. let intervalAverage = intervalArray.reduce(0, +) / Double(count)
  691. let maximumInterval = intervalArray.max()
  692. let minimumInterval = intervalArray.min()
  693. //
  694. let output = Loops(
  695. loops: Int(loopNr),
  696. errors: errorNR,
  697. success_rate: roundDecimal(Decimal(successRate ?? 0), 1),
  698. avg_interval: roundDecimal(Decimal(intervalAverage), 1),
  699. median_interval: roundDecimal(Decimal(median_interval), 1),
  700. min_interval: roundDecimal(Decimal(minimumInterval ?? 0), 1),
  701. max_interval: roundDecimal(Decimal(maximumInterval ?? 0), 1),
  702. avg_duration: roundDecimal(Decimal(durationAverage), 1),
  703. median_duration: roundDecimal(Decimal(medianDuration), 1),
  704. min_duration: roundDecimal(Decimal(min_duration), 1),
  705. max_duration: roundDecimal(Decimal(max_duration), 1)
  706. )
  707. return output
  708. }
  709. // fetch glucose for time interval
  710. func fetchGlucose(predicate: NSPredicate, fetchLimit: Int? = nil, batchSize: Int? = nil) async -> [GlucoseStored] {
  711. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  712. ofType: GlucoseStored.self,
  713. onContext: privateContext,
  714. predicate: predicate,
  715. key: "date",
  716. ascending: false,
  717. fetchLimit: fetchLimit,
  718. batchSize: batchSize
  719. )
  720. return await privateContext.perform {
  721. guard let glucoseResults = results as? [GlucoseStored] else {
  722. return []
  723. }
  724. return glucoseResults
  725. }
  726. }
  727. // TODO: - Refactor this whole shit here...
  728. // Add to statistics.JSON for upload to NS.
  729. private func statistics() async {
  730. let now = Date()
  731. if settingsManager.settings.uploadStats != nil {
  732. let hour = Calendar.current.component(.hour, from: now)
  733. guard hour > 20 else {
  734. return
  735. }
  736. // MARK: - Core Data related
  737. async let glucoseStats = glucoseForStats()
  738. async let lastLoopForStats = lastLoopForStats()
  739. async let carbTotal = carbsForStats()
  740. async let preferences = settingsManager.preferences
  741. let loopStats = await loopStats(oneDayGlucose: await glucoseStats.oneDayGlucose.readings)
  742. // Only save and upload once per day
  743. guard (-1 * (await lastLoopForStats ?? .distantPast).timeIntervalSinceNow.hours) > 22 else { return }
  744. let units = settingsManager.settings.units
  745. // MARK: - Not Core Data related stuff
  746. let pref = await preferences
  747. var algo_ = "Oref0"
  748. if pref.sigmoid, pref.enableDynamicCR {
  749. algo_ = "Dynamic ISF + CR: Sigmoid"
  750. } else if pref.sigmoid, !pref.enableDynamicCR {
  751. algo_ = "Dynamic ISF: Sigmoid"
  752. } else if pref.useNewFormula, pref.enableDynamicCR {
  753. algo_ = "Dynamic ISF + CR: Logarithmic"
  754. } else if pref.useNewFormula, !pref.sigmoid,!pref.enableDynamicCR {
  755. algo_ = "Dynamic ISF: Logarithmic"
  756. }
  757. let af = pref.adjustmentFactor
  758. let insulin_type = pref.curve
  759. // let buildDate = Bundle.main.buildDate // TODO: fix this
  760. let version = Bundle.main.releaseVersionNumber
  761. let build = Bundle.main.buildVersionNumber
  762. // Read branch information from branch.txt instead of infoDictionary
  763. var branch = "Unknown"
  764. if let branchFileURL = Bundle.main.url(forResource: "branch", withExtension: "txt"),
  765. let branchFileContent = try? String(contentsOf: branchFileURL)
  766. {
  767. let lines = branchFileContent.components(separatedBy: .newlines)
  768. for line in lines {
  769. let components = line.components(separatedBy: "=")
  770. if components.count == 2 {
  771. let key = components[0].trimmingCharacters(in: .whitespaces)
  772. let value = components[1].trimmingCharacters(in: .whitespaces)
  773. if key == "BRANCH" {
  774. branch = value
  775. break
  776. }
  777. }
  778. }
  779. } else {
  780. branch = "Unknown"
  781. }
  782. let copyrightNotice_ = Bundle.main.infoDictionary?["NSHumanReadableCopyright"] as? String ?? ""
  783. let pump_ = pumpManager?.localizedTitle ?? ""
  784. let cgm = settingsManager.settings.cgm
  785. let file = OpenAPS.Monitor.statistics
  786. var iPa: Decimal = 75
  787. if pref.useCustomPeakTime {
  788. iPa = pref.insulinPeakTime
  789. } else if pref.curve.rawValue == "rapid-acting" {
  790. iPa = 65
  791. } else if pref.curve.rawValue == "ultra-rapid" {
  792. iPa = 50
  793. }
  794. // Insulin placeholder
  795. let insulin = Ins(
  796. TDD: 0,
  797. bolus: 0,
  798. temp_basal: 0,
  799. scheduled_basal: 0,
  800. total_average: 0
  801. )
  802. let processedGlucoseStats = await glucoseStats
  803. let hbA1cDisplayUnit = processedGlucoseStats.hbA1cDisplayUnit
  804. let dailystat = await Statistics(
  805. created_at: Date(),
  806. iPhone: UIDevice.current.getDeviceId,
  807. iOS: UIDevice.current.getOSInfo,
  808. Build_Version: version ?? "",
  809. Build_Number: build ?? "1",
  810. Branch: branch,
  811. CopyRightNotice: String(copyrightNotice_.prefix(32)),
  812. Build_Date: Date(), // TODO: fix this
  813. Algorithm: algo_,
  814. AdjustmentFactor: af,
  815. Pump: pump_,
  816. CGM: cgm.rawValue,
  817. insulinType: insulin_type.rawValue,
  818. peakActivityTime: iPa,
  819. Carbs_24h: await carbTotal,
  820. GlucoseStorage_Days: Decimal(roundDouble(processedGlucoseStats.numberofDays, 1)),
  821. Statistics: Stats(
  822. Distribution: processedGlucoseStats.TimeInRange,
  823. Glucose: processedGlucoseStats.avg,
  824. HbA1c: processedGlucoseStats.hbs,
  825. Units: Units(Glucose: units.rawValue, HbA1c: hbA1cDisplayUnit.rawValue),
  826. LoopCycles: loopStats,
  827. Insulin: insulin,
  828. Variance: processedGlucoseStats.variance
  829. )
  830. )
  831. storage.save(dailystat, as: file)
  832. await saveStatsToCoreData()
  833. }
  834. }
  835. private func saveStatsToCoreData() async {
  836. await privateContext.perform {
  837. let saveStatsCoreData = StatsData(context: self.privateContext)
  838. saveStatsCoreData.lastrun = Date()
  839. do {
  840. guard self.privateContext.hasChanges else { return }
  841. try self.privateContext.save()
  842. } catch {
  843. print(error.localizedDescription)
  844. }
  845. }
  846. }
  847. private func lastLoopForStats() async -> Date? {
  848. let requestStats = StatsData.fetchRequest() as NSFetchRequest<StatsData>
  849. let sortStats = NSSortDescriptor(key: "lastrun", ascending: false)
  850. requestStats.sortDescriptors = [sortStats]
  851. requestStats.fetchLimit = 1
  852. return await privateContext.perform {
  853. do {
  854. return try self.privateContext.fetch(requestStats).first?.lastrun
  855. } catch {
  856. print(error.localizedDescription)
  857. return .distantPast
  858. }
  859. }
  860. }
  861. private func carbsForStats() async -> Decimal {
  862. let requestCarbs = CarbEntryStored.fetchRequest() as NSFetchRequest<CarbEntryStored>
  863. let daysAgo = Date().addingTimeInterval(-1.days.timeInterval)
  864. requestCarbs.predicate = NSPredicate(format: "carbs > 0 AND date > %@", daysAgo as NSDate)
  865. requestCarbs.sortDescriptors = [NSSortDescriptor(key: "date", ascending: true)]
  866. return await privateContext.perform {
  867. do {
  868. let carbs = try self.privateContext.fetch(requestCarbs)
  869. debugPrint(
  870. "APSManager: statistics() -> \(CoreDataStack.identifier) \(DebuggingIdentifiers.succeeded) fetched carbs"
  871. )
  872. return carbs.reduce(0) { sum, meal in
  873. let mealCarbs = Decimal(string: "\(meal.carbs)") ?? Decimal.zero
  874. return sum + mealCarbs
  875. }
  876. } catch {
  877. debugPrint(
  878. "APSManager: statistics() -> \(CoreDataStack.identifier) \(DebuggingIdentifiers.failed) error while fetching carbs"
  879. )
  880. return 0
  881. }
  882. }
  883. }
  884. private func loopStats(oneDayGlucose: Double) async -> LoopCycles {
  885. let requestLSR = LoopStatRecord.fetchRequest() as NSFetchRequest<LoopStatRecord>
  886. requestLSR.predicate = NSPredicate(
  887. format: "interval > 0 AND start > %@",
  888. Date().addingTimeInterval(-24.hours.timeInterval) as NSDate
  889. )
  890. let sortLSR = NSSortDescriptor(key: "start", ascending: false)
  891. requestLSR.sortDescriptors = [sortLSR]
  892. return await privateContext.perform {
  893. do {
  894. let lsr = try self.privateContext.fetch(requestLSR)
  895. // Compute LoopStats for 24 hours
  896. let oneDayLoops = self.loops(lsr)
  897. return LoopCycles(
  898. loops: oneDayLoops.loops,
  899. errors: oneDayLoops.errors,
  900. readings: Int(oneDayGlucose),
  901. success_rate: oneDayLoops.success_rate,
  902. avg_interval: oneDayLoops.avg_interval,
  903. median_interval: oneDayLoops.median_interval,
  904. min_interval: oneDayLoops.min_interval,
  905. max_interval: oneDayLoops.max_interval,
  906. avg_duration: oneDayLoops.avg_duration,
  907. median_duration: oneDayLoops.median_duration,
  908. min_duration: oneDayLoops.max_duration,
  909. max_duration: oneDayLoops.max_duration
  910. )
  911. } catch {
  912. debugPrint(
  913. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to get Loop statistics for Statistics Upload"
  914. )
  915. return LoopCycles(
  916. loops: 0,
  917. errors: 0,
  918. readings: 0,
  919. success_rate: 0,
  920. avg_interval: 0,
  921. median_interval: 0,
  922. min_interval: 0,
  923. max_interval: 0,
  924. avg_duration: 0,
  925. median_duration: 0,
  926. min_duration: 0,
  927. max_duration: 0
  928. )
  929. }
  930. }
  931. }
  932. private func tddForStats() async -> (currentTDD: Decimal, tddTotalAverage: Decimal) {
  933. let requestTDD = OrefDetermination.fetchRequest() as NSFetchRequest<NSFetchRequestResult>
  934. let sort = NSSortDescriptor(key: "timestamp", ascending: false)
  935. let daysOf14Ago = Date().addingTimeInterval(-14.days.timeInterval)
  936. requestTDD.predicate = NSPredicate(format: "timestamp > %@", daysOf14Ago as NSDate)
  937. requestTDD.sortDescriptors = [sort]
  938. requestTDD.propertiesToFetch = ["timestamp", "totalDailyDose"]
  939. requestTDD.resultType = .dictionaryResultType
  940. var currentTDD: Decimal = 0
  941. var tddTotalAverage: Decimal = 0
  942. let results = await privateContext.perform {
  943. do {
  944. let fetchedResults = try self.privateContext.fetch(requestTDD) as? [[String: Any]]
  945. return fetchedResults ?? []
  946. } catch {
  947. debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to get TDD Data for Statistics Upload")
  948. return []
  949. }
  950. }
  951. if !results.isEmpty {
  952. if let latestTDD = results.first?["totalDailyDose"] as? NSDecimalNumber {
  953. currentTDD = latestTDD.decimalValue
  954. }
  955. let tddArray = results.compactMap { ($0["totalDailyDose"] as? NSDecimalNumber)?.decimalValue }
  956. if !tddArray.isEmpty {
  957. tddTotalAverage = tddArray.reduce(0, +) / Decimal(tddArray.count)
  958. }
  959. }
  960. return (currentTDD, tddTotalAverage)
  961. }
  962. private func glucoseForStats() async
  963. -> (
  964. oneDayGlucose: (
  965. ifcc: Double,
  966. ngsp: Double,
  967. average: Double,
  968. median: Double,
  969. sd: Double,
  970. cv: Double,
  971. readings: Double
  972. ),
  973. hbA1cDisplayUnit: HbA1cDisplayUnit,
  974. numberofDays: Double,
  975. TimeInRange: TIRs,
  976. avg: Averages,
  977. hbs: Durations,
  978. variance: Variance
  979. )
  980. {
  981. // Get the Glucose Values
  982. let glucose24h = await fetchGlucose(predicate: NSPredicate.predicateForOneDayAgo, fetchLimit: 288, batchSize: 50)
  983. let glucoseOneWeek = await fetchGlucose(
  984. predicate: NSPredicate.predicateForOneWeek,
  985. fetchLimit: 288 * 7,
  986. batchSize: 250
  987. )
  988. let glucoseOneMonth = await fetchGlucose(
  989. predicate: NSPredicate.predicateForOneMonth,
  990. fetchLimit: 288 * 7 * 30,
  991. batchSize: 500
  992. )
  993. let glucoseThreeMonths = await fetchGlucose(
  994. predicate: NSPredicate.predicateForThreeMonths,
  995. fetchLimit: 288 * 7 * 30 * 3,
  996. batchSize: 1000
  997. )
  998. var result: (
  999. oneDayGlucose: (
  1000. ifcc: Double,
  1001. ngsp: Double,
  1002. average: Double,
  1003. median: Double,
  1004. sd: Double,
  1005. cv: Double,
  1006. readings: Double
  1007. ),
  1008. hbA1cDisplayUnit: HbA1cDisplayUnit,
  1009. numberofDays: Double,
  1010. TimeInRange: TIRs,
  1011. avg: Averages,
  1012. hbs: Durations,
  1013. variance: Variance
  1014. )?
  1015. await privateContext.perform {
  1016. let units = self.settingsManager.settings.units
  1017. // First date
  1018. let previous = glucoseThreeMonths.last?.date ?? Date()
  1019. // Last date (recent)
  1020. let current = glucoseThreeMonths.first?.date ?? Date()
  1021. // Total time in days
  1022. let numberOfDays = (current - previous).timeInterval / 8.64E4
  1023. // Get glucose computations for every case
  1024. let oneDayGlucose = self.glucoseStats(glucose24h)
  1025. let sevenDaysGlucose = self.glucoseStats(glucoseOneWeek)
  1026. let thirtyDaysGlucose = self.glucoseStats(glucoseOneMonth)
  1027. let totalDaysGlucose = self.glucoseStats(glucoseThreeMonths)
  1028. let median = Durations(
  1029. day: self.roundDecimal(Decimal(oneDayGlucose.median), 1),
  1030. week: self.roundDecimal(Decimal(sevenDaysGlucose.median), 1),
  1031. month: self.roundDecimal(Decimal(thirtyDaysGlucose.median), 1),
  1032. total: self.roundDecimal(Decimal(totalDaysGlucose.median), 1)
  1033. )
  1034. let hbA1cDisplayUnit = self.settingsManager.settings.hbA1cDisplayUnit
  1035. let hbs = Durations(
  1036. day: hbA1cDisplayUnit == .mmolMol ?
  1037. self.roundDecimal(Decimal(oneDayGlucose.ifcc), 1) :
  1038. self.roundDecimal(Decimal(oneDayGlucose.ngsp), 1),
  1039. week: hbA1cDisplayUnit == .mmolMol ?
  1040. self.roundDecimal(Decimal(sevenDaysGlucose.ifcc), 1) :
  1041. self.roundDecimal(Decimal(sevenDaysGlucose.ngsp), 1),
  1042. month: hbA1cDisplayUnit == .mmolMol ?
  1043. self.roundDecimal(Decimal(thirtyDaysGlucose.ifcc), 1) :
  1044. self.roundDecimal(Decimal(thirtyDaysGlucose.ngsp), 1),
  1045. total: hbA1cDisplayUnit == .mmolMol ?
  1046. self.roundDecimal(Decimal(totalDaysGlucose.ifcc), 1) :
  1047. self.roundDecimal(Decimal(totalDaysGlucose.ngsp), 1)
  1048. )
  1049. var oneDay_: (TIR: Double, hypos: Double, hypers: Double, normal_: Double) = (0.0, 0.0, 0.0, 0.0)
  1050. var sevenDays_: (TIR: Double, hypos: Double, hypers: Double, normal_: Double) = (0.0, 0.0, 0.0, 0.0)
  1051. var thirtyDays_: (TIR: Double, hypos: Double, hypers: Double, normal_: Double) = (0.0, 0.0, 0.0, 0.0)
  1052. var totalDays_: (TIR: Double, hypos: Double, hypers: Double, normal_: Double) = (0.0, 0.0, 0.0, 0.0)
  1053. // Get TIR computations for every case
  1054. oneDay_ = self.tir(glucose24h)
  1055. sevenDays_ = self.tir(glucoseOneWeek)
  1056. thirtyDays_ = self.tir(glucoseOneMonth)
  1057. totalDays_ = self.tir(glucoseThreeMonths)
  1058. let tir = Durations(
  1059. day: self.roundDecimal(Decimal(oneDay_.TIR), 1),
  1060. week: self.roundDecimal(Decimal(sevenDays_.TIR), 1),
  1061. month: self.roundDecimal(Decimal(thirtyDays_.TIR), 1),
  1062. total: self.roundDecimal(Decimal(totalDays_.TIR), 1)
  1063. )
  1064. let hypo = Durations(
  1065. day: Decimal(oneDay_.hypos),
  1066. week: Decimal(sevenDays_.hypos),
  1067. month: Decimal(thirtyDays_.hypos),
  1068. total: Decimal(totalDays_.hypos)
  1069. )
  1070. let hyper = Durations(
  1071. day: Decimal(oneDay_.hypers),
  1072. week: Decimal(sevenDays_.hypers),
  1073. month: Decimal(thirtyDays_.hypers),
  1074. total: Decimal(totalDays_.hypers)
  1075. )
  1076. let normal = Durations(
  1077. day: Decimal(oneDay_.normal_),
  1078. week: Decimal(sevenDays_.normal_),
  1079. month: Decimal(thirtyDays_.normal_),
  1080. total: Decimal(totalDays_.normal_)
  1081. )
  1082. let range = Threshold(
  1083. low: units == .mmolL ? self.roundDecimal(self.settingsManager.settings.low.asMmolL, 1) :
  1084. self.roundDecimal(self.settingsManager.settings.low, 0),
  1085. high: units == .mmolL ? self.roundDecimal(self.settingsManager.settings.high.asMmolL, 1) :
  1086. self.roundDecimal(self.settingsManager.settings.high, 0)
  1087. )
  1088. let TimeInRange = TIRs(
  1089. TIR: tir,
  1090. Hypos: hypo,
  1091. Hypers: hyper,
  1092. Threshold: range,
  1093. Euglycemic: normal
  1094. )
  1095. let avgs = Durations(
  1096. day: self.roundDecimal(Decimal(oneDayGlucose.average), 1),
  1097. week: self.roundDecimal(Decimal(sevenDaysGlucose.average), 1),
  1098. month: self.roundDecimal(Decimal(thirtyDaysGlucose.average), 1),
  1099. total: self.roundDecimal(Decimal(totalDaysGlucose.average), 1)
  1100. )
  1101. let avg = Averages(Average: avgs, Median: median)
  1102. // Standard Deviations
  1103. let standardDeviations = Durations(
  1104. day: self.roundDecimal(Decimal(oneDayGlucose.sd), 1),
  1105. week: self.roundDecimal(Decimal(sevenDaysGlucose.sd), 1),
  1106. month: self.roundDecimal(Decimal(thirtyDaysGlucose.sd), 1),
  1107. total: self.roundDecimal(Decimal(totalDaysGlucose.sd), 1)
  1108. )
  1109. // CV = standard deviation / sample mean x 100
  1110. let cvs = Durations(
  1111. day: self.roundDecimal(Decimal(oneDayGlucose.cv), 1),
  1112. week: self.roundDecimal(Decimal(sevenDaysGlucose.cv), 1),
  1113. month: self.roundDecimal(Decimal(thirtyDaysGlucose.cv), 1),
  1114. total: self.roundDecimal(Decimal(totalDaysGlucose.cv), 1)
  1115. )
  1116. let variance = Variance(SD: standardDeviations, CV: cvs)
  1117. result = (oneDayGlucose, hbA1cDisplayUnit, numberOfDays, TimeInRange, avg, hbs, variance)
  1118. }
  1119. return result!
  1120. }
  1121. private func loopStats(loopStatRecord: LoopStats) {
  1122. privateContext.perform {
  1123. let nLS = LoopStatRecord(context: self.privateContext)
  1124. nLS.start = loopStatRecord.start
  1125. nLS.end = loopStatRecord.end ?? Date()
  1126. nLS.loopStatus = loopStatRecord.loopStatus
  1127. nLS.duration = loopStatRecord.duration ?? 0.0
  1128. nLS.interval = loopStatRecord.interval ?? 0.0
  1129. do {
  1130. guard self.privateContext.hasChanges else { return }
  1131. try self.privateContext.save()
  1132. } catch {
  1133. print(error.localizedDescription)
  1134. }
  1135. }
  1136. }
  1137. private func processError(_ error: Error) {
  1138. warning(.apsManager, "\(error.localizedDescription)")
  1139. lastError.send(error)
  1140. }
  1141. private func createBolusReporter() {
  1142. bolusReporter = pumpManager?.createBolusProgressReporter(reportingOn: processQueue)
  1143. bolusReporter?.addObserver(self)
  1144. }
  1145. private func clearBolusReporter() {
  1146. bolusReporter?.removeObserver(self)
  1147. bolusReporter = nil
  1148. processQueue.asyncAfter(deadline: .now() + 0.5) {
  1149. self.bolusProgress.send(nil)
  1150. }
  1151. }
  1152. }
  1153. private extension PumpManager {
  1154. func enactTempBasal(unitsPerHour: Double, for duration: TimeInterval) async throws {
  1155. try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
  1156. self.enactTempBasal(unitsPerHour: unitsPerHour, for: duration) { error in
  1157. if let error = error {
  1158. debug(.apsManager, "Temp basal failed: \(unitsPerHour) for: \(duration)")
  1159. continuation.resume(throwing: error)
  1160. } else {
  1161. debug(.apsManager, "Temp basal succeeded: \(unitsPerHour) for: \(duration)")
  1162. continuation.resume(returning: ())
  1163. }
  1164. }
  1165. }
  1166. }
  1167. func enactBolus(units: Double, automatic: Bool) async throws {
  1168. try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
  1169. let automaticValue = automatic ? BolusActivationType.automatic : BolusActivationType.manualRecommendationAccepted
  1170. self.enactBolus(units: units, activationType: automaticValue) { error in
  1171. if let error = error {
  1172. debug(.apsManager, "Bolus failed: \(units)")
  1173. continuation.resume(throwing: error)
  1174. } else {
  1175. debug(.apsManager, "Bolus succeeded: \(units)")
  1176. continuation.resume(returning: ())
  1177. }
  1178. }
  1179. }
  1180. }
  1181. func cancelBolus() async throws -> DoseEntry? {
  1182. try await withCheckedThrowingContinuation { continuation in
  1183. self.cancelBolus { result in
  1184. switch result {
  1185. case let .success(dose):
  1186. debug(.apsManager, "Cancel Bolus succeeded")
  1187. continuation.resume(returning: dose)
  1188. case let .failure(error):
  1189. debug(.apsManager, "Cancel Bolus failed")
  1190. continuation.resume(throwing: APSError.pumpError(error))
  1191. }
  1192. }
  1193. }
  1194. }
  1195. func suspendDelivery() async throws {
  1196. try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
  1197. self.suspendDelivery { error in
  1198. if let error = error {
  1199. continuation.resume(throwing: error)
  1200. } else {
  1201. continuation.resume()
  1202. }
  1203. }
  1204. }
  1205. }
  1206. func resumeDelivery() async throws {
  1207. try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
  1208. self.resumeDelivery { error in
  1209. if let error = error {
  1210. continuation.resume(throwing: error)
  1211. } else {
  1212. continuation.resume()
  1213. }
  1214. }
  1215. }
  1216. }
  1217. }
  1218. extension BaseAPSManager: PumpManagerStatusObserver {
  1219. func pumpManager(_: PumpManager, didUpdate status: PumpManagerStatus, oldStatus _: PumpManagerStatus) {
  1220. let percent = Int((status.pumpBatteryChargeRemaining ?? 1) * 100)
  1221. privateContext.perform {
  1222. /// only update the last item with the current battery infos instead of saving a new one each time
  1223. let fetchRequest: NSFetchRequest<OpenAPS_Battery> = OpenAPS_Battery.fetchRequest()
  1224. fetchRequest.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)]
  1225. fetchRequest.predicate = NSPredicate.predicateFor30MinAgo
  1226. fetchRequest.fetchLimit = 1
  1227. do {
  1228. let results = try self.privateContext.fetch(fetchRequest)
  1229. let batteryToStore: OpenAPS_Battery
  1230. if let existingBattery = results.first {
  1231. batteryToStore = existingBattery
  1232. } else {
  1233. batteryToStore = OpenAPS_Battery(context: self.privateContext)
  1234. batteryToStore.id = UUID()
  1235. }
  1236. batteryToStore.date = Date()
  1237. batteryToStore.percent = Double(percent)
  1238. batteryToStore.voltage = nil
  1239. batteryToStore.status = percent > 10 ? "normal" : "low"
  1240. batteryToStore.display = status.pumpBatteryChargeRemaining != nil
  1241. guard self.privateContext.hasChanges else { return }
  1242. try self.privateContext.save()
  1243. } catch {
  1244. print("Failed to fetch or save battery: \(error.localizedDescription)")
  1245. }
  1246. }
  1247. // TODO: - remove this after ensuring that NS still gets the same infos from Core Data
  1248. storage.save(status.pumpStatus, as: OpenAPS.Monitor.status)
  1249. }
  1250. }
  1251. extension BaseAPSManager: DoseProgressObserver {
  1252. func doseProgressReporterDidUpdate(_ doseProgressReporter: DoseProgressReporter) {
  1253. bolusProgress.send(Decimal(doseProgressReporter.progress.percentComplete))
  1254. if doseProgressReporter.progress.isComplete {
  1255. clearBolusReporter()
  1256. }
  1257. }
  1258. }
  1259. extension PumpManagerStatus {
  1260. var pumpStatus: PumpStatus {
  1261. let bolusing = bolusState != .noBolus
  1262. let suspended = basalDeliveryState?.isSuspended ?? true
  1263. let type = suspended ? StatusType.suspended : (bolusing ? .bolusing : .normal)
  1264. return PumpStatus(status: type, bolusing: bolusing, suspended: suspended, timestamp: Date())
  1265. }
  1266. }