APSManager.swift 55 KB

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