APSManager.swift 60 KB

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