APSManager.swift 63 KB

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