APSManager.swift 61 KB

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