APSManager.swift 62 KB

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