APSManager.swift 60 KB

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