APSManager.swift 63 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496
  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. // TODO: - replace this...
  691. let saveLastLoop = LastLoop(context: self.privateContext)
  692. saveLastLoop.iob = (determinationUpdated.iob ?? 0) as NSDecimalNumber
  693. saveLastLoop.cob = determinationUpdated.cob as? NSDecimalNumber
  694. saveLastLoop.timestamp = (determinationUpdated.timestamp ?? .distantPast) as Date
  695. do {
  696. guard privateContext.hasChanges else { return }
  697. try privateContext.save()
  698. } catch {
  699. print(error.localizedDescription)
  700. }
  701. debug(.apsManager, "Determination enacted. Received: \(received)")
  702. nightscout.uploadStatus()
  703. statistics()
  704. } else {
  705. debugPrint("Failed to update OrefDetermination in reportEnacted()")
  706. }
  707. }
  708. }
  709. private func roundDecimal(_ decimal: Decimal, _ digits: Double) -> Decimal {
  710. let rounded = round(Double(decimal) * pow(10, digits)) / pow(10, digits)
  711. return Decimal(rounded)
  712. }
  713. private func roundDouble(_ double: Double, _ digits: Double) -> Double {
  714. let rounded = round(Double(double) * pow(10, digits)) / pow(10, digits)
  715. return rounded
  716. }
  717. private func medianCalculationDouble(array: [Double]) -> 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 (sorted[length / 2 - 1] + sorted[length / 2]) / 2
  725. }
  726. return sorted[length / 2]
  727. }
  728. private func medianCalculation(array: [Int]) -> Double {
  729. guard !array.isEmpty else {
  730. return 0
  731. }
  732. let sorted = array.sorted()
  733. let length = array.count
  734. if length % 2 == 0 {
  735. return Double((sorted[length / 2 - 1] + sorted[length / 2]) / 2)
  736. }
  737. return Double(sorted[length / 2])
  738. }
  739. private func tir(_ glucose: [GlucoseStored]) -> (TIR: Double, hypos: Double, hypers: Double, normal_: Double) {
  740. privateContext.perform {
  741. let justGlucoseArray = glucose.compactMap({ each in Int(each.glucose as Int16) })
  742. let totalReadings = justGlucoseArray.count
  743. let highLimit = settingsManager.settings.high
  744. let lowLimit = settingsManager.settings.low
  745. let hyperArray = glucose.filter({ $0.glucose >= Int(highLimit) })
  746. let hyperReadings = hyperArray.compactMap({ each in each.glucose as Int16 }).count
  747. let hyperPercentage = Double(hyperReadings) / Double(totalReadings) * 100
  748. let hypoArray = glucose.filter({ $0.glucose <= Int(lowLimit) })
  749. let hypoReadings = hypoArray.compactMap({ each in each.glucose as Int16 }).count
  750. let hypoPercentage = Double(hypoReadings) / Double(totalReadings) * 100
  751. // Euglyccemic range
  752. let normalArray = glucose.filter({ $0.glucose >= 70 && $0.glucose <= 140 })
  753. let normalReadings = normalArray.compactMap({ each in each.glucose as Int16 }).count
  754. let normalPercentage = Double(normalReadings) / Double(totalReadings) * 100
  755. // TIR
  756. let tir = 100 - (hypoPercentage + hyperPercentage)
  757. return (
  758. roundDouble(tir, 1),
  759. roundDouble(hypoPercentage, 1),
  760. roundDouble(hyperPercentage, 1),
  761. roundDouble(normalPercentage, 1)
  762. )
  763. }
  764. }
  765. private func glucoseStats(_ fetchedGlucose: [GlucoseStored])
  766. -> (ifcc: Double, ngsp: Double, average: Double, median: Double, sd: Double, cv: Double, readings: Double)
  767. {
  768. let glucose = fetchedGlucose
  769. // First date
  770. let last = glucose.last?.date ?? Date()
  771. // Last date (recent)
  772. let first = glucose.first?.date ?? Date()
  773. // Total time in days
  774. let numberOfDays = (first - last).timeInterval / 8.64E4
  775. let denominator = numberOfDays < 1 ? 1 : numberOfDays
  776. let justGlucoseArray = glucose.compactMap({ each in Int(each.glucose as Int16) })
  777. let sumReadings = justGlucoseArray.reduce(0, +)
  778. let countReadings = justGlucoseArray.count
  779. let glucoseAverage = Double(sumReadings) / Double(countReadings)
  780. let medianGlucose = medianCalculation(array: justGlucoseArray)
  781. var NGSPa1CStatisticValue = 0.0
  782. var IFCCa1CStatisticValue = 0.0
  783. NGSPa1CStatisticValue = (glucoseAverage + 46.7) / 28.7 // NGSP (%)
  784. IFCCa1CStatisticValue = 10.929 *
  785. (NGSPa1CStatisticValue - 2.152) // IFCC (mmol/mol) A1C(mmol/mol) = 10.929 * (A1C(%) - 2.15)
  786. var sumOfSquares = 0.0
  787. for array in justGlucoseArray {
  788. sumOfSquares += pow(Double(array) - Double(glucoseAverage), 2)
  789. }
  790. var sd = 0.0
  791. var cv = 0.0
  792. // Avoid division by zero
  793. if glucoseAverage > 0 {
  794. sd = sqrt(sumOfSquares / Double(countReadings))
  795. cv = sd / Double(glucoseAverage) * 100
  796. }
  797. let conversionFactor = 0.0555
  798. let units = settingsManager.settings.units
  799. var output: (ifcc: Double, ngsp: Double, average: Double, median: Double, sd: Double, cv: Double, readings: Double)
  800. output = (
  801. ifcc: IFCCa1CStatisticValue,
  802. ngsp: NGSPa1CStatisticValue,
  803. average: glucoseAverage * (units == .mmolL ? conversionFactor : 1),
  804. median: medianGlucose * (units == .mmolL ? conversionFactor : 1),
  805. sd: sd * (units == .mmolL ? conversionFactor : 1), cv: cv,
  806. readings: Double(countReadings) / denominator
  807. )
  808. return output
  809. }
  810. private func loops(_ fetchedLoops: [LoopStatRecord]) -> Loops {
  811. let loops = fetchedLoops
  812. // First date
  813. let previous = loops.last?.end ?? Date()
  814. // Last date (recent)
  815. let current = loops.first?.start ?? Date()
  816. // Total time in days
  817. let totalTime = (current - previous).timeInterval / 8.64E4
  818. //
  819. let durationArray = loops.compactMap({ each in each.duration })
  820. let durationArrayCount = durationArray.count
  821. let durationAverage = durationArray.reduce(0, +) / Double(durationArrayCount) * 60
  822. let medianDuration = medianCalculationDouble(array: durationArray) * 60
  823. let max_duration = (durationArray.max() ?? 0) * 60
  824. let min_duration = (durationArray.min() ?? 0) * 60
  825. let successsNR = loops.compactMap({ each in each.loopStatus }).filter({ each in each!.contains("Success") }).count
  826. let errorNR = durationArrayCount - successsNR
  827. let total = Double(successsNR + errorNR) == 0 ? 1 : Double(successsNR + errorNR)
  828. let successRate: Double? = (Double(successsNR) / total) * 100
  829. let loopNr = totalTime <= 1 ? total : round(total / (totalTime != 0 ? totalTime : 1))
  830. let intervalArray = loops.compactMap({ each in each.interval as Double })
  831. let count = intervalArray.count != 0 ? intervalArray.count : 1
  832. let median_interval = medianCalculationDouble(array: intervalArray)
  833. let intervalAverage = intervalArray.reduce(0, +) / Double(count)
  834. let maximumInterval = intervalArray.max()
  835. let minimumInterval = intervalArray.min()
  836. //
  837. let output = Loops(
  838. loops: Int(loopNr),
  839. errors: errorNR,
  840. success_rate: roundDecimal(Decimal(successRate ?? 0), 1),
  841. avg_interval: roundDecimal(Decimal(intervalAverage), 1),
  842. median_interval: roundDecimal(Decimal(median_interval), 1),
  843. min_interval: roundDecimal(Decimal(minimumInterval ?? 0), 1),
  844. max_interval: roundDecimal(Decimal(maximumInterval ?? 0), 1),
  845. avg_duration: roundDecimal(Decimal(durationAverage), 1),
  846. median_duration: roundDecimal(Decimal(medianDuration), 1),
  847. min_duration: roundDecimal(Decimal(min_duration), 1),
  848. max_duration: roundDecimal(Decimal(max_duration), 1)
  849. )
  850. return output
  851. }
  852. // fetch glucose for time interval
  853. func fetchGlucose(predicate: NSPredicate, fetchLimit: Int? = nil, batchSize: Int? = nil) -> [GlucoseStored] {
  854. CoreDataStack.shared.fetchEntities(
  855. ofType: GlucoseStored.self,
  856. onContext: privateContext,
  857. predicate: predicate,
  858. key: "date",
  859. ascending: false,
  860. fetchLimit: fetchLimit,
  861. batchSize: batchSize
  862. )
  863. }
  864. // Add to statistics.JSON for upload to NS.
  865. private func statistics() {
  866. let now = Date()
  867. if settingsManager.settings.uploadStats {
  868. let hour = Calendar.current.component(.hour, from: now)
  869. guard hour > 20 else {
  870. return
  871. }
  872. privateContext.perform { [self] in
  873. var stats = [StatsData]()
  874. let requestStats = StatsData.fetchRequest() as NSFetchRequest<StatsData>
  875. let sortStats = NSSortDescriptor(key: "lastrun", ascending: false)
  876. requestStats.sortDescriptors = [sortStats]
  877. requestStats.fetchLimit = 1
  878. try? stats = privateContext.fetch(requestStats)
  879. // Only save and upload once per day
  880. guard (-1 * (stats.first?.lastrun ?? .distantPast).timeIntervalSinceNow.hours) > 22 else { return }
  881. let units = self.settingsManager.settings.units
  882. let preferences = settingsManager.preferences
  883. // Carbs
  884. var carbTotal: Decimal = 0
  885. let requestCarbs = CarbEntryStored.fetchRequest() as NSFetchRequest<CarbEntryStored>
  886. let daysAgo = Date().addingTimeInterval(-1.days.timeInterval)
  887. requestCarbs.predicate = NSPredicate(format: "carbs > 0 AND date > %@", daysAgo as NSDate)
  888. requestCarbs.sortDescriptors = [NSSortDescriptor(key: "date", ascending: true)]
  889. do {
  890. let carbs = try privateContext.fetch(requestCarbs)
  891. carbTotal = carbs.reduce(0) { sum, meal in
  892. let mealCarbs = Decimal(string: "\(meal.carbs)") ?? Decimal.zero
  893. return sum + mealCarbs
  894. }
  895. debugPrint(
  896. "APSManager: statistics() -> \(CoreDataStack.identifier) \(DebuggingIdentifiers.succeeded) fetched carbs"
  897. )
  898. } catch {
  899. debugPrint(
  900. "APSManager: statistics() -> \(CoreDataStack.identifier) \(DebuggingIdentifiers.failed) error while fetching carbs"
  901. )
  902. }
  903. // TDD
  904. var tdds = [TDD]()
  905. var currentTDD: Decimal = 0
  906. var tddTotalAverage: Decimal = 0
  907. let requestTDD = TDD.fetchRequest() as NSFetchRequest<TDD>
  908. let sort = NSSortDescriptor(key: "timestamp", ascending: false)
  909. let daysOf14Ago = Date().addingTimeInterval(-14.days.timeInterval)
  910. requestTDD.predicate = NSPredicate(format: "timestamp > %@", daysOf14Ago as NSDate)
  911. requestTDD.sortDescriptors = [sort]
  912. try? tdds = privateContext.fetch(requestTDD)
  913. if !tdds.isEmpty {
  914. currentTDD = tdds[0].tdd?.decimalValue ?? 0
  915. let tddArray = tdds.compactMap({ insulin in insulin.tdd as? Decimal ?? 0 })
  916. tddTotalAverage = tddArray.reduce(0, +) / Decimal(tddArray.count)
  917. }
  918. var algo_ = "Oref0"
  919. if preferences.sigmoid, preferences.enableDynamicCR {
  920. algo_ = "Dynamic ISF + CR: Sigmoid"
  921. } else if preferences.sigmoid, !preferences.enableDynamicCR {
  922. algo_ = "Dynamic ISF: Sigmoid"
  923. } else if preferences.useNewFormula, preferences.enableDynamicCR {
  924. algo_ = "Dynamic ISF + CR: Logarithmic"
  925. } else if preferences.useNewFormula, !preferences.sigmoid,!preferences.enableDynamicCR {
  926. algo_ = "Dynamic ISF: Logarithmic"
  927. }
  928. let af = preferences.adjustmentFactor
  929. let insulin_type = preferences.curve
  930. let buildDate = Bundle.main.buildDate
  931. let version = Bundle.main.releaseVersionNumber
  932. let build = Bundle.main.buildVersionNumber
  933. // Read branch information from branch.txt instead of infoDictionary
  934. var branch = "Unknown"
  935. if let branchFileURL = Bundle.main.url(forResource: "branch", withExtension: "txt"),
  936. let branchFileContent = try? String(contentsOf: branchFileURL)
  937. {
  938. let lines = branchFileContent.components(separatedBy: .newlines)
  939. for line in lines {
  940. let components = line.components(separatedBy: "=")
  941. if components.count == 2 {
  942. let key = components[0].trimmingCharacters(in: .whitespaces)
  943. let value = components[1].trimmingCharacters(in: .whitespaces)
  944. if key == "BRANCH" {
  945. branch = value
  946. break
  947. }
  948. }
  949. }
  950. } else {
  951. branch = "Unknown"
  952. }
  953. let copyrightNotice_ = Bundle.main.infoDictionary?["NSHumanReadableCopyright"] as? String ?? ""
  954. let pump_ = pumpManager?.localizedTitle ?? ""
  955. let cgm = settingsManager.settings.cgm
  956. let file = OpenAPS.Monitor.statistics
  957. var iPa: Decimal = 75
  958. if preferences.useCustomPeakTime {
  959. iPa = preferences.insulinPeakTime
  960. } else if preferences.curve.rawValue == "rapid-acting" {
  961. iPa = 65
  962. } else if preferences.curve.rawValue == "ultra-rapid" {
  963. iPa = 50
  964. }
  965. // Glucose Values
  966. let glucose24h = fetchGlucose(predicate: NSPredicate.predicateForOneDayAgo, fetchLimit: 288, batchSize: 50)
  967. let glucoseOneWeek = fetchGlucose(predicate: NSPredicate.predicateForOneWeek, fetchLimit: 288 * 7, batchSize: 250)
  968. let glucoseOneMonth = fetchGlucose(
  969. predicate: NSPredicate.predicateForOneMonth,
  970. fetchLimit: 288 * 7 * 30,
  971. batchSize: 500
  972. )
  973. let glucoseThreeMonths = fetchGlucose(
  974. predicate: NSPredicate.predicateForThreeMonths,
  975. fetchLimit: 288 * 7 * 30 * 3,
  976. batchSize: 1000
  977. )
  978. // First date
  979. let previous = glucoseThreeMonths.last?.date ?? Date()
  980. // Last date (recent)
  981. let current = glucoseThreeMonths.first?.date ?? Date()
  982. // Total time in days
  983. let numberOfDays = (current - previous).timeInterval / 8.64E4
  984. // Get glucose computations for every case
  985. let oneDayGlucose = glucoseStats(glucose24h)
  986. let sevenDaysGlucose = glucoseStats(glucoseOneWeek)
  987. let thirtyDaysGlucose = glucoseStats(glucoseOneMonth)
  988. let totalDaysGlucose = glucoseStats(glucoseThreeMonths)
  989. let median = Durations(
  990. day: roundDecimal(Decimal(oneDayGlucose.median), 1),
  991. week: roundDecimal(Decimal(sevenDaysGlucose.median), 1),
  992. month: roundDecimal(Decimal(thirtyDaysGlucose.median), 1),
  993. total: roundDecimal(Decimal(totalDaysGlucose.median), 1)
  994. )
  995. let overrideHbA1cUnit = settingsManager.settings.overrideHbA1cUnit
  996. let hbs = Durations(
  997. day: ((units == .mmolL && !overrideHbA1cUnit) || (units == .mgdL && overrideHbA1cUnit)) ?
  998. roundDecimal(Decimal(oneDayGlucose.ifcc), 1) : roundDecimal(Decimal(oneDayGlucose.ngsp), 1),
  999. week: ((units == .mmolL && !overrideHbA1cUnit) || (units == .mgdL && overrideHbA1cUnit)) ?
  1000. roundDecimal(Decimal(sevenDaysGlucose.ifcc), 1) : roundDecimal(Decimal(sevenDaysGlucose.ngsp), 1),
  1001. month: ((units == .mmolL && !overrideHbA1cUnit) || (units == .mgdL && overrideHbA1cUnit)) ?
  1002. roundDecimal(Decimal(thirtyDaysGlucose.ifcc), 1) : roundDecimal(Decimal(thirtyDaysGlucose.ngsp), 1),
  1003. total: ((units == .mmolL && !overrideHbA1cUnit) || (units == .mgdL && overrideHbA1cUnit)) ?
  1004. roundDecimal(Decimal(totalDaysGlucose.ifcc), 1) : roundDecimal(Decimal(totalDaysGlucose.ngsp), 1)
  1005. )
  1006. var oneDay_: (TIR: Double, hypos: Double, hypers: Double, normal_: Double) = (0.0, 0.0, 0.0, 0.0)
  1007. var sevenDays_: (TIR: Double, hypos: Double, hypers: Double, normal_: Double) = (0.0, 0.0, 0.0, 0.0)
  1008. var thirtyDays_: (TIR: Double, hypos: Double, hypers: Double, normal_: Double) = (0.0, 0.0, 0.0, 0.0)
  1009. var totalDays_: (TIR: Double, hypos: Double, hypers: Double, normal_: Double) = (0.0, 0.0, 0.0, 0.0)
  1010. // Get TIR computations for every case
  1011. oneDay_ = tir(glucose24h)
  1012. sevenDays_ = tir(glucoseOneWeek)
  1013. thirtyDays_ = tir(glucoseOneMonth)
  1014. totalDays_ = tir(glucoseThreeMonths)
  1015. let tir = Durations(
  1016. day: roundDecimal(Decimal(oneDay_.TIR), 1),
  1017. week: roundDecimal(Decimal(sevenDays_.TIR), 1),
  1018. month: roundDecimal(Decimal(thirtyDays_.TIR), 1),
  1019. total: roundDecimal(Decimal(totalDays_.TIR), 1)
  1020. )
  1021. let hypo = Durations(
  1022. day: Decimal(oneDay_.hypos),
  1023. week: Decimal(sevenDays_.hypos),
  1024. month: Decimal(thirtyDays_.hypos),
  1025. total: Decimal(totalDays_.hypos)
  1026. )
  1027. let hyper = Durations(
  1028. day: Decimal(oneDay_.hypers),
  1029. week: Decimal(sevenDays_.hypers),
  1030. month: Decimal(thirtyDays_.hypers),
  1031. total: Decimal(totalDays_.hypers)
  1032. )
  1033. let normal = Durations(
  1034. day: Decimal(oneDay_.normal_),
  1035. week: Decimal(sevenDays_.normal_),
  1036. month: Decimal(thirtyDays_.normal_),
  1037. total: Decimal(totalDays_.normal_)
  1038. )
  1039. let range = Threshold(
  1040. low: units == .mmolL ? roundDecimal(settingsManager.settings.low.asMmolL, 1) :
  1041. roundDecimal(settingsManager.settings.low, 0),
  1042. high: units == .mmolL ? roundDecimal(settingsManager.settings.high.asMmolL, 1) :
  1043. roundDecimal(settingsManager.settings.high, 0)
  1044. )
  1045. let TimeInRange = TIRs(
  1046. TIR: tir,
  1047. Hypos: hypo,
  1048. Hypers: hyper,
  1049. Threshold: range,
  1050. Euglycemic: normal
  1051. )
  1052. let avgs = Durations(
  1053. day: roundDecimal(Decimal(oneDayGlucose.average), 1),
  1054. week: roundDecimal(Decimal(sevenDaysGlucose.average), 1),
  1055. month: roundDecimal(Decimal(thirtyDaysGlucose.average), 1),
  1056. total: roundDecimal(Decimal(totalDaysGlucose.average), 1)
  1057. )
  1058. let avg = Averages(Average: avgs, Median: median)
  1059. // Standard Deviations
  1060. let standardDeviations = Durations(
  1061. day: roundDecimal(Decimal(oneDayGlucose.sd), 1),
  1062. week: roundDecimal(Decimal(sevenDaysGlucose.sd), 1),
  1063. month: roundDecimal(Decimal(thirtyDaysGlucose.sd), 1),
  1064. total: roundDecimal(Decimal(totalDaysGlucose.sd), 1)
  1065. )
  1066. // CV = standard deviation / sample mean x 100
  1067. let cvs = Durations(
  1068. day: roundDecimal(Decimal(oneDayGlucose.cv), 1),
  1069. week: roundDecimal(Decimal(sevenDaysGlucose.cv), 1),
  1070. month: roundDecimal(Decimal(thirtyDaysGlucose.cv), 1),
  1071. total: roundDecimal(Decimal(totalDaysGlucose.cv), 1)
  1072. )
  1073. let variance = Variance(SD: standardDeviations, CV: cvs)
  1074. // Loops
  1075. var lsr = [LoopStatRecord]()
  1076. let requestLSR = LoopStatRecord.fetchRequest() as NSFetchRequest<LoopStatRecord>
  1077. requestLSR.predicate = NSPredicate(
  1078. format: "interval > 0 AND start > %@",
  1079. Date().addingTimeInterval(-24.hours.timeInterval) as NSDate
  1080. )
  1081. let sortLSR = NSSortDescriptor(key: "start", ascending: false)
  1082. requestLSR.sortDescriptors = [sortLSR]
  1083. try? lsr = privateContext.fetch(requestLSR)
  1084. // Compute LoopStats for 24 hours
  1085. let oneDayLoops = loops(lsr)
  1086. let loopstat = LoopCycles(
  1087. loops: oneDayLoops.loops,
  1088. errors: oneDayLoops.errors,
  1089. readings: Int(oneDayGlucose.readings),
  1090. success_rate: oneDayLoops.success_rate,
  1091. avg_interval: oneDayLoops.avg_interval,
  1092. median_interval: oneDayLoops.median_interval,
  1093. min_interval: oneDayLoops.min_interval,
  1094. max_interval: oneDayLoops.max_interval,
  1095. avg_duration: oneDayLoops.avg_duration,
  1096. median_duration: oneDayLoops.median_duration,
  1097. min_duration: oneDayLoops.max_duration,
  1098. max_duration: oneDayLoops.max_duration
  1099. )
  1100. // Insulin
  1101. var insulinDistribution = [InsulinDistribution]()
  1102. var insulin = Ins(
  1103. TDD: 0,
  1104. bolus: 0,
  1105. temp_basal: 0,
  1106. scheduled_basal: 0,
  1107. total_average: 0
  1108. )
  1109. let requestInsulinDistribution = InsulinDistribution.fetchRequest() as NSFetchRequest<InsulinDistribution>
  1110. let sortInsulin = NSSortDescriptor(key: "date", ascending: false)
  1111. requestInsulinDistribution.sortDescriptors = [sortInsulin]
  1112. try? insulinDistribution = privateContext.fetch(requestInsulinDistribution)
  1113. insulin = Ins(
  1114. TDD: roundDecimal(currentTDD, 2),
  1115. bolus: insulinDistribution.first != nil ? ((insulinDistribution.first?.bolus ?? 0) as Decimal) : 0,
  1116. temp_basal: insulinDistribution.first != nil ? ((insulinDistribution.first?.tempBasal ?? 0) as Decimal) : 0,
  1117. scheduled_basal: insulinDistribution
  1118. .first != nil ? ((insulinDistribution.first?.scheduledBasal ?? 0) as Decimal) : 0,
  1119. total_average: roundDecimal(tddTotalAverage, 1)
  1120. )
  1121. let hbA1cUnit = !overrideHbA1cUnit ? (units == .mmolL ? "mmol/mol" : "%") : (units == .mmolL ? "%" : "mmol/mol")
  1122. let dailystat = Statistics(
  1123. created_at: Date(),
  1124. iPhone: UIDevice.current.getDeviceId,
  1125. iOS: UIDevice.current.getOSInfo,
  1126. Build_Version: version ?? "",
  1127. Build_Number: build ?? "1",
  1128. Branch: branch,
  1129. CopyRightNotice: String(copyrightNotice_.prefix(32)),
  1130. Build_Date: buildDate,
  1131. Algorithm: algo_,
  1132. AdjustmentFactor: af,
  1133. Pump: pump_,
  1134. CGM: cgm.rawValue,
  1135. insulinType: insulin_type.rawValue,
  1136. peakActivityTime: iPa,
  1137. Carbs_24h: carbTotal,
  1138. GlucoseStorage_Days: Decimal(roundDouble(numberOfDays, 1)),
  1139. Statistics: Stats(
  1140. Distribution: TimeInRange,
  1141. Glucose: avg,
  1142. HbA1c: hbs, Units: Units(Glucose: units.rawValue, HbA1c: hbA1cUnit),
  1143. LoopCycles: loopstat,
  1144. Insulin: insulin,
  1145. Variance: variance
  1146. )
  1147. )
  1148. storage.save(dailystat, as: file)
  1149. nightscout.uploadStatistics(dailystat: dailystat)
  1150. let saveStatsCoreData = StatsData(context: self.privateContext)
  1151. saveStatsCoreData.lastrun = Date()
  1152. do {
  1153. guard self.privateContext.hasChanges else { return }
  1154. try self.privateContext.save()
  1155. } catch {
  1156. print(error.localizedDescription)
  1157. }
  1158. }
  1159. }
  1160. }
  1161. private func loopStats(loopStatRecord: LoopStats) {
  1162. privateContext.perform {
  1163. let nLS = LoopStatRecord(context: self.privateContext)
  1164. nLS.start = loopStatRecord.start
  1165. nLS.end = loopStatRecord.end ?? Date()
  1166. nLS.loopStatus = loopStatRecord.loopStatus
  1167. nLS.duration = loopStatRecord.duration ?? 0.0
  1168. nLS.interval = loopStatRecord.interval ?? 0.0
  1169. do {
  1170. guard self.privateContext.hasChanges else { return }
  1171. try self.privateContext.save()
  1172. } catch {
  1173. print(error.localizedDescription)
  1174. }
  1175. }
  1176. }
  1177. private func processError(_ error: Error) {
  1178. warning(.apsManager, "\(error.localizedDescription)")
  1179. lastError.send(error)
  1180. }
  1181. private func createBolusReporter() {
  1182. bolusReporter = pumpManager?.createBolusProgressReporter(reportingOn: processQueue)
  1183. bolusReporter?.addObserver(self)
  1184. }
  1185. private func updateStatus() {
  1186. debug(.apsManager, "force update status")
  1187. guard let pump = pumpManager else {
  1188. return
  1189. }
  1190. if let omnipod = pump as? OmnipodPumpManager {
  1191. omnipod.getPodStatus { _ in }
  1192. }
  1193. if let omnipodBLE = pump as? OmniBLEPumpManager {
  1194. omnipodBLE.getPodStatus { _ in }
  1195. }
  1196. }
  1197. private func clearBolusReporter() {
  1198. bolusReporter?.removeObserver(self)
  1199. bolusReporter = nil
  1200. processQueue.asyncAfter(deadline: .now() + 0.5) {
  1201. self.bolusProgress.send(nil)
  1202. self.updateStatus()
  1203. }
  1204. }
  1205. }
  1206. private extension PumpManager {
  1207. func enactTempBasal(unitsPerHour: Double, for duration: TimeInterval) async throws {
  1208. try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
  1209. self.enactTempBasal(unitsPerHour: unitsPerHour, for: duration) { error in
  1210. if let error = error {
  1211. debug(.apsManager, "Temp basal failed: \(unitsPerHour) for: \(duration)")
  1212. continuation.resume(throwing: error)
  1213. } else {
  1214. debug(.apsManager, "Temp basal succeeded: \(unitsPerHour) for: \(duration)")
  1215. continuation.resume(returning: ())
  1216. }
  1217. }
  1218. }
  1219. }
  1220. func enactBolus(units: Double, automatic: Bool) async throws {
  1221. try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
  1222. let automaticValue = automatic ? BolusActivationType.automatic : BolusActivationType.manualRecommendationAccepted
  1223. self.enactBolus(units: units, activationType: automaticValue) { error in
  1224. if let error = error {
  1225. debug(.apsManager, "Bolus failed: \(units)")
  1226. continuation.resume(throwing: error)
  1227. } else {
  1228. debug(.apsManager, "Bolus succeeded: \(units)")
  1229. continuation.resume(returning: ())
  1230. }
  1231. }
  1232. }
  1233. }
  1234. func cancelBolus() async throws -> DoseEntry? {
  1235. try await withCheckedThrowingContinuation { continuation in
  1236. self.cancelBolus { result in
  1237. switch result {
  1238. case let .success(dose):
  1239. debug(.apsManager, "Cancel Bolus succeeded")
  1240. continuation.resume(returning: dose)
  1241. case let .failure(error):
  1242. debug(.apsManager, "Cancel Bolus failed")
  1243. continuation.resume(throwing: APSError.pumpError(error))
  1244. }
  1245. }
  1246. }
  1247. }
  1248. func suspendDelivery() -> AnyPublisher<Void, Error> {
  1249. Future { promise in
  1250. self.suspendDelivery { 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. func resumeDelivery() -> AnyPublisher<Void, Error> {
  1262. Future { promise in
  1263. self.resumeDelivery { error in
  1264. if let error = error {
  1265. promise(.failure(error))
  1266. } else {
  1267. promise(.success(()))
  1268. }
  1269. }
  1270. }
  1271. .mapError { APSError.pumpError($0) }
  1272. .eraseToAnyPublisher()
  1273. }
  1274. }
  1275. extension BaseAPSManager: PumpManagerStatusObserver {
  1276. func pumpManager(_: PumpManager, didUpdate status: PumpManagerStatus, oldStatus _: PumpManagerStatus) {
  1277. let percent = Int((status.pumpBatteryChargeRemaining ?? 1) * 100)
  1278. privateContext.perform {
  1279. /// only update the last item with the current battery infos instead of saving a new one each time
  1280. let fetchRequest: NSFetchRequest<OpenAPS_Battery> = OpenAPS_Battery.fetchRequest()
  1281. fetchRequest.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)]
  1282. fetchRequest.predicate = NSPredicate.predicateFor30MinAgo
  1283. fetchRequest.fetchLimit = 1
  1284. do {
  1285. let results = try self.privateContext.fetch(fetchRequest)
  1286. let batteryToStore: OpenAPS_Battery
  1287. if let existingBattery = results.first {
  1288. batteryToStore = existingBattery
  1289. } else {
  1290. batteryToStore = OpenAPS_Battery(context: self.privateContext)
  1291. batteryToStore.id = UUID()
  1292. }
  1293. batteryToStore.date = Date()
  1294. batteryToStore.percent = Int16(percent)
  1295. batteryToStore.voltage = nil
  1296. batteryToStore.status = percent > 10 ? "normal" : "low"
  1297. batteryToStore.display = status.pumpBatteryChargeRemaining != nil
  1298. guard self.privateContext.hasChanges else { return }
  1299. try self.privateContext.save()
  1300. } catch {
  1301. print("Failed to fetch or save battery: \(error.localizedDescription)")
  1302. }
  1303. }
  1304. // TODO: - remove this after ensuring that NS still gets the same infos from Core Data
  1305. storage.save(status.pumpStatus, as: OpenAPS.Monitor.status)
  1306. }
  1307. }
  1308. extension BaseAPSManager: DoseProgressObserver {
  1309. func doseProgressReporterDidUpdate(_ doseProgressReporter: DoseProgressReporter) {
  1310. bolusProgress.send(Decimal(doseProgressReporter.progress.percentComplete))
  1311. if doseProgressReporter.progress.isComplete {
  1312. clearBolusReporter()
  1313. }
  1314. }
  1315. }
  1316. extension PumpManagerStatus {
  1317. var pumpStatus: PumpStatus {
  1318. let bolusing = bolusState != .noBolus
  1319. let suspended = basalDeliveryState?.isSuspended ?? true
  1320. let type = suspended ? StatusType.suspended : (bolusing ? .bolusing : .normal)
  1321. return PumpStatus(status: type, bolusing: bolusing, suspended: suspended, timestamp: Date())
  1322. }
  1323. }