APSManager.swift 61 KB

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