APSManager.swift 57 KB

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