APSManager.swift 69 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751
  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. let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
  81. private var openAPS: OpenAPS!
  82. private var lifetime = Lifetime()
  83. private var backGroundTaskID: UIBackgroundTaskIdentifier?
  84. var pumpManager: PumpManagerUI? {
  85. get { deviceDataManager.pumpManager }
  86. set { deviceDataManager.pumpManager = newValue }
  87. }
  88. var bluetoothManager: BluetoothStateManager? { deviceDataManager.bluetoothManager }
  89. @Persisted(key: "isManualTempBasal") var isManualTempBasal: Bool = false
  90. let isLooping = CurrentValueSubject<Bool, Never>(false)
  91. let lastLoopDateSubject = PassthroughSubject<Date, Never>()
  92. let lastError = CurrentValueSubject<Error?, Never>(nil)
  93. let bolusProgress = CurrentValueSubject<Decimal?, Never>(nil)
  94. var pumpDisplayState: CurrentValueSubject<PumpDisplayState?, Never> {
  95. deviceDataManager.pumpDisplayState
  96. }
  97. var pumpName: CurrentValueSubject<String, Never> {
  98. deviceDataManager.pumpName
  99. }
  100. var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> {
  101. deviceDataManager.pumpExpiresAtDate
  102. }
  103. var settings: FreeAPSSettings {
  104. get { settingsManager.settings }
  105. set { settingsManager.settings = newValue }
  106. }
  107. init(resolver: Resolver) {
  108. injectServices(resolver)
  109. openAPS = OpenAPS(storage: storage)
  110. subscribe()
  111. lastLoopDateSubject.send(lastLoopDate)
  112. isLooping
  113. .weakAssign(to: \.deviceDataManager.loopInProgress, on: self)
  114. .store(in: &lifetime)
  115. }
  116. private func subscribe() {
  117. deviceDataManager.recommendsLoop
  118. .receive(on: processQueue)
  119. .sink { [weak self] in
  120. self?.loop()
  121. }
  122. .store(in: &lifetime)
  123. pumpManager?.addStatusObserver(self, queue: processQueue)
  124. deviceDataManager.errorSubject
  125. .receive(on: processQueue)
  126. .map { APSError.pumpError($0) }
  127. .sink {
  128. self.processError($0)
  129. }
  130. .store(in: &lifetime)
  131. deviceDataManager.bolusTrigger
  132. .receive(on: processQueue)
  133. .sink { bolusing in
  134. if bolusing {
  135. self.createBolusReporter()
  136. } else {
  137. self.clearBolusReporter()
  138. }
  139. }
  140. .store(in: &lifetime)
  141. // manage a manual Temp Basal from OmniPod - Force loop() after stop a temp basal or finished
  142. deviceDataManager.manualTempBasal
  143. .receive(on: processQueue)
  144. .sink { manualBasal in
  145. if manualBasal {
  146. self.isManualTempBasal = true
  147. } else {
  148. if self.isManualTempBasal {
  149. self.isManualTempBasal = false
  150. self.loop()
  151. }
  152. }
  153. }
  154. .store(in: &lifetime)
  155. }
  156. func heartbeat(date: Date) {
  157. deviceDataManager.heartbeat(date: date)
  158. }
  159. // Loop entry point
  160. private func loop() {
  161. // check the last start of looping is more the loopInterval but the previous loop was completed
  162. if lastLoopDate > lastStartLoopDate {
  163. guard lastStartLoopDate.addingTimeInterval(Config.loopInterval) < Date() else {
  164. debug(.apsManager, "too close to do a loop : \(lastStartLoopDate)")
  165. return
  166. }
  167. }
  168. guard !isLooping.value else {
  169. warning(.apsManager, "Loop already in progress. Skip recommendation.")
  170. return
  171. }
  172. // start background time extension
  173. backGroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "Loop starting") {
  174. guard let backgroundTask = self.backGroundTaskID else { return }
  175. UIApplication.shared.endBackgroundTask(backgroundTask)
  176. self.backGroundTaskID = .invalid
  177. }
  178. debug(.apsManager, "Starting loop with a delay of \(UIApplication.shared.backgroundTimeRemaining.rounded())")
  179. lastStartLoopDate = Date()
  180. var loopStatRecord = LoopStats(
  181. start: lastStartLoopDate,
  182. loopStatus: "Starting"
  183. )
  184. isLooping.send(true)
  185. determineBasal()
  186. .replaceEmpty(with: false)
  187. .flatMap { [weak self] success -> AnyPublisher<Void, Error> in
  188. guard let self = self, success else {
  189. return Fail(error: APSError.apsError(message: "Determine basal failed")).eraseToAnyPublisher()
  190. }
  191. // Open loop completed
  192. guard self.settings.closedLoop else {
  193. self.nightscout.uploadStatus()
  194. return Just(()).setFailureType(to: Error.self).eraseToAnyPublisher()
  195. }
  196. self.nightscout.uploadStatus()
  197. // Closed loop - enact suggested
  198. return self.enactSuggested()
  199. }
  200. .sink { [weak self] completion in
  201. guard let self = self else { return }
  202. loopStatRecord.end = Date()
  203. loopStatRecord.duration = self.roundDouble(
  204. (loopStatRecord.end! - loopStatRecord.start).timeInterval / 60,
  205. 2
  206. )
  207. if case let .failure(error) = completion {
  208. loopStatRecord.loopStatus = error.localizedDescription
  209. self.loopCompleted(error: error, loopStatRecord: loopStatRecord)
  210. } else {
  211. loopStatRecord.loopStatus = "Success"
  212. self.loopCompleted(loopStatRecord: loopStatRecord)
  213. }
  214. } receiveValue: {}
  215. .store(in: &lifetime)
  216. }
  217. // Loop exit point
  218. private func loopCompleted(error: Error? = nil, loopStatRecord: LoopStats) {
  219. isLooping.send(false)
  220. // save AH events
  221. let events = pumpHistoryStorage.recent()
  222. healthKitManager.saveIfNeeded(pumpEvents: events)
  223. if let error = error {
  224. warning(.apsManager, "Loop failed with error: \(error.localizedDescription)")
  225. if let backgroundTask = backGroundTaskID {
  226. UIApplication.shared.endBackgroundTask(backgroundTask)
  227. backGroundTaskID = .invalid
  228. }
  229. processError(error)
  230. } else {
  231. debug(.apsManager, "Loop succeeded")
  232. lastLoopDate = Date()
  233. lastError.send(nil)
  234. }
  235. loopStats(loopStatRecord: loopStatRecord)
  236. if settings.closedLoop {
  237. reportEnacted(received: error == nil)
  238. }
  239. // end of the BG tasks
  240. if let backgroundTask = backGroundTaskID {
  241. UIApplication.shared.endBackgroundTask(backgroundTask)
  242. backGroundTaskID = .invalid
  243. }
  244. }
  245. private func verifyStatus() -> Error? {
  246. guard let pump = pumpManager else {
  247. return APSError.invalidPumpState(message: "Pump not set")
  248. }
  249. let status = pump.status.pumpStatus
  250. guard !status.bolusing else {
  251. return APSError.invalidPumpState(message: "Pump is bolusing")
  252. }
  253. guard !status.suspended else {
  254. return APSError.invalidPumpState(message: "Pump suspended")
  255. }
  256. let reservoir = storage.retrieve(OpenAPS.Monitor.reservoir, as: Decimal.self) ?? 100
  257. guard reservoir >= 0 else {
  258. return APSError.invalidPumpState(message: "Reservoir is empty")
  259. }
  260. return nil
  261. }
  262. private func autosens() -> AnyPublisher<Bool, Never> {
  263. guard let autosens = storage.retrieve(OpenAPS.Settings.autosense, as: Autosens.self),
  264. (autosens.timestamp ?? .distantPast).addingTimeInterval(30.minutes.timeInterval) > Date()
  265. else {
  266. return openAPS.autosense()
  267. .map { $0 != nil }
  268. .eraseToAnyPublisher()
  269. }
  270. return Just(false).eraseToAnyPublisher()
  271. }
  272. func determineBasal() -> AnyPublisher<Bool, Never> {
  273. debug(.apsManager, "Start determine basal")
  274. guard let glucose = storage.retrieve(OpenAPS.Monitor.glucose, as: [BloodGlucose].self), glucose.isNotEmpty else {
  275. debug(.apsManager, "Not enough glucose data")
  276. processError(APSError.glucoseError(message: "Not enough glucose data"))
  277. return Just(false).eraseToAnyPublisher()
  278. }
  279. let lastGlucoseDate = glucoseStorage.lastGlucoseDate()
  280. guard lastGlucoseDate >= Date().addingTimeInterval(-12.minutes.timeInterval) else {
  281. debug(.apsManager, "Glucose data is stale")
  282. processError(APSError.glucoseError(message: "Glucose data is stale"))
  283. return Just(false).eraseToAnyPublisher()
  284. }
  285. guard glucoseStorage.isGlucoseNotFlat() else {
  286. debug(.apsManager, "Glucose data is too flat")
  287. processError(APSError.glucoseError(message: "Glucose data is too flat"))
  288. return Just(false).eraseToAnyPublisher()
  289. }
  290. let now = Date()
  291. let temp = currentTemp(date: now)
  292. let mainPublisher = makeProfiles()
  293. .flatMap { _ in self.autosens() }
  294. .flatMap { _ in self.dailyAutotune() }
  295. .flatMap { _ in self.openAPS.determineBasal(currentTemp: temp, clock: now) }
  296. .map { suggestion -> Bool in
  297. if let suggestion = suggestion {
  298. DispatchQueue.main.async {
  299. self.broadcaster.notify(SuggestionObserver.self, on: .main) {
  300. $0.suggestionDidUpdate(suggestion)
  301. }
  302. }
  303. }
  304. return suggestion != nil
  305. }
  306. .eraseToAnyPublisher()
  307. if temp.duration == 0,
  308. settings.closedLoop,
  309. settingsManager.preferences.unsuspendIfNoTemp,
  310. let pump = pumpManager,
  311. pump.status.pumpStatus.suspended
  312. {
  313. return pump.resumeDelivery()
  314. .flatMap { _ in mainPublisher }
  315. .replaceError(with: false)
  316. .eraseToAnyPublisher()
  317. }
  318. return mainPublisher
  319. }
  320. func determineBasalSync() {
  321. determineBasal().cancellable().store(in: &lifetime)
  322. }
  323. func makeProfiles() -> AnyPublisher<Bool, Never> {
  324. openAPS.makeProfiles(useAutotune: settings.useAutotune)
  325. .map { tunedProfile in
  326. if let basalProfile = tunedProfile?.basalProfile {
  327. self.processQueue.async {
  328. self.broadcaster.notify(BasalProfileObserver.self, on: self.processQueue) {
  329. $0.basalProfileDidChange(basalProfile)
  330. }
  331. }
  332. }
  333. return tunedProfile != nil
  334. }
  335. .eraseToAnyPublisher()
  336. }
  337. func roundBolus(amount: Decimal) -> Decimal {
  338. guard let pump = pumpManager else { return amount }
  339. let rounded = Decimal(pump.roundToSupportedBolusVolume(units: Double(amount)))
  340. let maxBolus = Decimal(pump.roundToSupportedBolusVolume(units: Double(settingsManager.pumpSettings.maxBolus)))
  341. return min(rounded, maxBolus)
  342. }
  343. private var bolusReporter: DoseProgressReporter?
  344. func enactBolus(amount: Double, isSMB: Bool) {
  345. if let error = verifyStatus() {
  346. processError(error)
  347. processQueue.async {
  348. self.broadcaster.notify(BolusFailureObserver.self, on: self.processQueue) {
  349. $0.bolusDidFail()
  350. }
  351. }
  352. return
  353. }
  354. guard let pump = pumpManager else { return }
  355. let roundedAmout = pump.roundToSupportedBolusVolume(units: amount)
  356. debug(.apsManager, "Enact bolus \(roundedAmout), manual \(!isSMB)")
  357. pump.enactBolus(units: roundedAmout, automatic: isSMB).sink { completion in
  358. if case let .failure(error) = completion {
  359. warning(.apsManager, "Bolus failed with error: \(error.localizedDescription)")
  360. self.processError(APSError.pumpError(error))
  361. if !isSMB {
  362. self.processQueue.async {
  363. self.broadcaster.notify(BolusFailureObserver.self, on: self.processQueue) {
  364. $0.bolusDidFail()
  365. }
  366. }
  367. }
  368. } else {
  369. debug(.apsManager, "Bolus succeeded")
  370. if !isSMB {
  371. self.determineBasal().sink { _ in }.store(in: &self.lifetime)
  372. }
  373. self.bolusProgress.send(0)
  374. }
  375. } receiveValue: { _ in }
  376. .store(in: &lifetime)
  377. }
  378. func cancelBolus() {
  379. guard let pump = pumpManager, pump.status.pumpStatus.bolusing else { return }
  380. debug(.apsManager, "Cancel bolus")
  381. pump.cancelBolus().sink { completion in
  382. if case let .failure(error) = completion {
  383. debug(.apsManager, "Bolus cancellation failed with error: \(error.localizedDescription)")
  384. self.processError(APSError.pumpError(error))
  385. } else {
  386. debug(.apsManager, "Bolus cancelled")
  387. }
  388. self.bolusReporter?.removeObserver(self)
  389. self.bolusReporter = nil
  390. self.bolusProgress.send(nil)
  391. } receiveValue: { _ in }
  392. .store(in: &lifetime)
  393. }
  394. func enactTempBasal(rate: Double, duration: TimeInterval) {
  395. if let error = verifyStatus() {
  396. processError(error)
  397. return
  398. }
  399. guard let pump = pumpManager else { return }
  400. // unable to do temp basal during manual temp basal 😁
  401. if isManualTempBasal {
  402. processError(APSError.manualBasalTemp(message: "Loop not possible during the manual basal temp"))
  403. return
  404. }
  405. debug(.apsManager, "Enact temp basal \(rate) - \(duration)")
  406. let roundedAmout = pump.roundToSupportedBasalRate(unitsPerHour: rate)
  407. pump.enactTempBasal(unitsPerHour: roundedAmout, for: duration) { error in
  408. if let error = error {
  409. debug(.apsManager, "Temp Basal failed with error: \(error.localizedDescription)")
  410. self.processError(APSError.pumpError(error))
  411. } else {
  412. debug(.apsManager, "Temp Basal succeeded")
  413. let temp = TempBasal(duration: Int(duration / 60), rate: Decimal(rate), temp: .absolute, timestamp: Date())
  414. self.storage.save(temp, as: OpenAPS.Monitor.tempBasal)
  415. if rate == 0, duration == 0 {
  416. self.pumpHistoryStorage.saveCancelTempEvents()
  417. }
  418. }
  419. }
  420. }
  421. func dailyAutotune() -> AnyPublisher<Bool, Never> {
  422. guard settings.useAutotune else {
  423. return Just(false).eraseToAnyPublisher()
  424. }
  425. let now = Date()
  426. guard lastAutotuneDate.isBeforeDate(now, granularity: .day) else {
  427. return Just(false).eraseToAnyPublisher()
  428. }
  429. lastAutotuneDate = now
  430. return autotune().map { $0 != nil }.eraseToAnyPublisher()
  431. }
  432. func autotune() -> AnyPublisher<Autotune?, Never> {
  433. openAPS.autotune().eraseToAnyPublisher()
  434. }
  435. func enactAnnouncement(_ announcement: Announcement) {
  436. guard let action = announcement.action else {
  437. warning(.apsManager, "Invalid Announcement action")
  438. return
  439. }
  440. guard let pump = pumpManager else {
  441. warning(.apsManager, "Pump is not set")
  442. return
  443. }
  444. debug(.apsManager, "Start enact announcement: \(action)")
  445. switch action {
  446. case let .bolus(amount):
  447. if let error = verifyStatus() {
  448. processError(error)
  449. return
  450. }
  451. let roundedAmount = pump.roundToSupportedBolusVolume(units: Double(amount))
  452. pump.enactBolus(units: roundedAmount, activationType: .manualRecommendationAccepted) { error in
  453. if let error = error {
  454. // warning(.apsManager, "Announcement Bolus failed with error: \(error.localizedDescription)")
  455. switch error {
  456. case .uncertainDelivery:
  457. // Do not generate notification on uncertain delivery error
  458. break
  459. default:
  460. // Do not generate notifications for automatic boluses that fail.
  461. warning(.apsManager, "Announcement Bolus failed with error: \(error.localizedDescription)")
  462. }
  463. } else {
  464. debug(.apsManager, "Announcement Bolus succeeded")
  465. self.announcementsStorage.storeAnnouncements([announcement], enacted: true)
  466. self.bolusProgress.send(0)
  467. }
  468. }
  469. case let .pump(pumpAction):
  470. switch pumpAction {
  471. case .suspend:
  472. if let error = verifyStatus() {
  473. processError(error)
  474. return
  475. }
  476. pump.suspendDelivery { error in
  477. if let error = error {
  478. debug(.apsManager, "Pump not suspended by Announcement: \(error.localizedDescription)")
  479. } else {
  480. debug(.apsManager, "Pump suspended by Announcement")
  481. self.announcementsStorage.storeAnnouncements([announcement], enacted: true)
  482. self.nightscout.uploadStatus()
  483. }
  484. }
  485. case .resume:
  486. guard pump.status.pumpStatus.suspended else {
  487. return
  488. }
  489. pump.resumeDelivery { error in
  490. if let error = error {
  491. warning(.apsManager, "Pump not resumed by Announcement: \(error.localizedDescription)")
  492. } else {
  493. debug(.apsManager, "Pump resumed by Announcement")
  494. self.announcementsStorage.storeAnnouncements([announcement], enacted: true)
  495. self.nightscout.uploadStatus()
  496. }
  497. }
  498. }
  499. case let .looping(closedLoop):
  500. settings.closedLoop = closedLoop
  501. debug(.apsManager, "Closed loop \(closedLoop) by Announcement")
  502. announcementsStorage.storeAnnouncements([announcement], enacted: true)
  503. case let .tempbasal(rate, duration):
  504. if let error = verifyStatus() {
  505. processError(error)
  506. return
  507. }
  508. // unable to do temp basal during manual temp basal 😁
  509. if isManualTempBasal {
  510. processError(APSError.manualBasalTemp(message: "Loop not possible during the manual basal temp"))
  511. return
  512. }
  513. guard !settings.closedLoop else {
  514. return
  515. }
  516. let roundedRate = pump.roundToSupportedBasalRate(unitsPerHour: Double(rate))
  517. pump.enactTempBasal(unitsPerHour: roundedRate, for: TimeInterval(duration) * 60) { error in
  518. if let error = error {
  519. warning(.apsManager, "Announcement TempBasal failed with error: \(error.localizedDescription)")
  520. } else {
  521. debug(.apsManager, "Announcement TempBasal succeeded")
  522. self.announcementsStorage.storeAnnouncements([announcement], enacted: true)
  523. }
  524. }
  525. }
  526. }
  527. private func currentTemp(date: Date) -> TempBasal {
  528. let defaultTemp = { () -> TempBasal in
  529. guard let temp = storage.retrieve(OpenAPS.Monitor.tempBasal, as: TempBasal.self) else {
  530. return TempBasal(duration: 0, rate: 0, temp: .absolute, timestamp: Date())
  531. }
  532. let delta = Int((date.timeIntervalSince1970 - temp.timestamp.timeIntervalSince1970) / 60)
  533. let duration = max(0, temp.duration - delta)
  534. return TempBasal(duration: duration, rate: temp.rate, temp: .absolute, timestamp: date)
  535. }()
  536. guard let state = pumpManager?.status.basalDeliveryState else { return defaultTemp }
  537. switch state {
  538. case .active:
  539. return TempBasal(duration: 0, rate: 0, temp: .absolute, timestamp: date)
  540. case let .tempBasal(dose):
  541. let rate = Decimal(dose.unitsPerHour)
  542. let durationMin = max(0, Int((dose.endDate.timeIntervalSince1970 - date.timeIntervalSince1970) / 60))
  543. return TempBasal(duration: durationMin, rate: rate, temp: .absolute, timestamp: date)
  544. default:
  545. return defaultTemp
  546. }
  547. }
  548. private func enactSuggested() -> AnyPublisher<Void, Error> {
  549. guard let suggested = storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self) else {
  550. return Fail(error: APSError.apsError(message: "Suggestion not found")).eraseToAnyPublisher()
  551. }
  552. guard Date().timeIntervalSince(suggested.deliverAt ?? .distantPast) < Config.eхpirationInterval else {
  553. return Fail(error: APSError.apsError(message: "Suggestion expired")).eraseToAnyPublisher()
  554. }
  555. guard let pump = pumpManager else {
  556. return Fail(error: APSError.apsError(message: "Pump not set")).eraseToAnyPublisher()
  557. }
  558. // unable to do temp basal during manual temp basal 😁
  559. if isManualTempBasal {
  560. return Fail(error: APSError.manualBasalTemp(message: "Loop not possible during the manual basal temp"))
  561. .eraseToAnyPublisher()
  562. }
  563. let basalPublisher: AnyPublisher<Void, Error> = Deferred { () -> AnyPublisher<Void, Error> in
  564. if let error = self.verifyStatus() {
  565. return Fail(error: error).eraseToAnyPublisher()
  566. }
  567. guard let rate = suggested.rate, let duration = suggested.duration else {
  568. // It is OK, no temp required
  569. debug(.apsManager, "No temp required")
  570. return Just(()).setFailureType(to: Error.self)
  571. .eraseToAnyPublisher()
  572. }
  573. return pump.enactTempBasal(unitsPerHour: Double(rate), for: TimeInterval(duration * 60)).map { _ in
  574. let temp = TempBasal(duration: duration, rate: rate, temp: .absolute, timestamp: Date())
  575. self.storage.save(temp, as: OpenAPS.Monitor.tempBasal)
  576. return ()
  577. }
  578. .eraseToAnyPublisher()
  579. }.eraseToAnyPublisher()
  580. let bolusPublisher: AnyPublisher<Void, Error> = Deferred { () -> AnyPublisher<Void, Error> in
  581. if let error = self.verifyStatus() {
  582. return Fail(error: error).eraseToAnyPublisher()
  583. }
  584. guard let units = suggested.units else {
  585. // It is OK, no bolus required
  586. debug(.apsManager, "No bolus required")
  587. return Just(()).setFailureType(to: Error.self)
  588. .eraseToAnyPublisher()
  589. }
  590. return pump.enactBolus(units: Double(units), automatic: true).map { _ in
  591. self.bolusProgress.send(0)
  592. return ()
  593. }
  594. .eraseToAnyPublisher()
  595. }.eraseToAnyPublisher()
  596. return basalPublisher.flatMap { bolusPublisher }.eraseToAnyPublisher()
  597. }
  598. private func reportEnacted(received: Bool) {
  599. if let suggestion = storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self), suggestion.deliverAt != nil {
  600. var enacted = suggestion
  601. enacted.timestamp = Date()
  602. enacted.recieved = received
  603. storage.save(enacted, as: OpenAPS.Enact.enacted)
  604. debug(.apsManager, "Suggestion enacted. Received: \(received)")
  605. DispatchQueue.main.async {
  606. self.broadcaster.notify(EnactedSuggestionObserver.self, on: .main) {
  607. $0.enactedSuggestionDidUpdate(enacted)
  608. }
  609. }
  610. nightscout.uploadStatus()
  611. // Update the TDD value
  612. tdd(enacted_: enacted)
  613. // Update statistics
  614. statistics()
  615. }
  616. }
  617. private func tdd(enacted_: Suggestion) {
  618. let tddStartedAt = Date()
  619. let preferences = settingsManager.preferences
  620. let currentTDD = enacted_.tdd ?? 0
  621. var booleanArray = [TempTargetsSlider]()
  622. var overrideArray = [Override]()
  623. var tempTargetsArray = [TempTargets]()
  624. var isPercentageEnabled = false
  625. var useOverride = false
  626. var overridePercentage: Decimal = 100
  627. var duration: Decimal = 0
  628. var unlimited: Bool = false
  629. var newDuration: Decimal = 0
  630. var hbtSetting: Decimal = 160
  631. if currentTDD > 0 {
  632. let tenDaysAgo = Date().addingTimeInterval(-10.days.timeInterval)
  633. let twoHoursAgo = Date().addingTimeInterval(-2.hours.timeInterval)
  634. var uniqEvents = [TDD]()
  635. var total: Decimal = 0
  636. var totalAmount: Decimal = 0
  637. var indeces: Int = 0
  638. var nrOfIndeces: Int = 0
  639. coredataContext.performAndWait {
  640. let requestTDD = TDD.fetchRequest() as NSFetchRequest<TDD>
  641. requestTDD.predicate = NSPredicate(format: "timestamp > %@ AND tdd > 0", tenDaysAgo as NSDate)
  642. let sortTDD = NSSortDescriptor(key: "timestamp", ascending: true)
  643. requestTDD.sortDescriptors = [sortTDD]
  644. try? uniqEvents = coredataContext.fetch(requestTDD)
  645. let requestIsEnbled = TempTargetsSlider.fetchRequest() as NSFetchRequest<TempTargetsSlider>
  646. let sortIsEnabled = NSSortDescriptor(key: "date", ascending: false)
  647. requestIsEnbled.sortDescriptors = [sortIsEnabled]
  648. // requestIsEnbled.fetchLimit = 1
  649. try? booleanArray = coredataContext.fetch(requestIsEnbled)
  650. let requestOverrides = Override.fetchRequest() as NSFetchRequest<Override>
  651. let sortOverride = NSSortDescriptor(key: "date", ascending: false)
  652. requestOverrides.sortDescriptors = [sortOverride]
  653. requestOverrides.fetchLimit = 1
  654. try? overrideArray = coredataContext.fetch(requestOverrides)
  655. let requestTempTargets = TempTargets.fetchRequest() as NSFetchRequest<TempTargets>
  656. let sortTT = NSSortDescriptor(key: "date", ascending: false)
  657. requestTempTargets.sortDescriptors = [sortTT]
  658. requestTempTargets.fetchLimit = 1
  659. try? tempTargetsArray = coredataContext.fetch(requestTempTargets)
  660. total = uniqEvents.compactMap({ each in each.tdd as? Decimal ?? 0 }).reduce(0, +)
  661. indeces = uniqEvents.count
  662. // Only fetch once. Use same (previous) fetch
  663. let twoHoursArray = uniqEvents.filter({ ($0.timestamp ?? Date()) >= twoHoursAgo })
  664. nrOfIndeces = twoHoursArray.count
  665. totalAmount = twoHoursArray.compactMap({ each in each.tdd as? Decimal ?? 0 }).reduce(0, +)
  666. }
  667. if indeces == 0 {
  668. indeces = 1
  669. }
  670. if nrOfIndeces == 0 {
  671. nrOfIndeces = 1
  672. }
  673. let average2hours = totalAmount / Decimal(nrOfIndeces)
  674. let average14 = total / Decimal(indeces)
  675. let weight = preferences.weightPercentage
  676. let weighted_average = weight * average2hours + (1 - weight) * average14
  677. isPercentageEnabled = booleanArray.first?.enabled ?? false
  678. useOverride = overrideArray.first?.enabled ?? false
  679. overridePercentage = Decimal(overrideArray.first?.percentage ?? 100)
  680. unlimited = overrideArray.first?.indefinite ?? true
  681. hbtSetting = Decimal(booleanArray.first?.hbt ?? 160)
  682. if useOverride {
  683. duration = (overrideArray.first?.duration ?? 0) as Decimal
  684. let addedMinutes = Int(duration)
  685. let date = overrideArray.first?.date ?? Date()
  686. if date.addingTimeInterval(addedMinutes.minutes.timeInterval) < Date(),
  687. !unlimited
  688. { useOverride = false }
  689. newDuration = Decimal(Date().distance(to: date.addingTimeInterval(addedMinutes.minutes.timeInterval)).minutes)
  690. }
  691. if newDuration < 0 {
  692. newDuration = 0
  693. } else { duration = newDuration }
  694. if !useOverride {
  695. unlimited = true
  696. overridePercentage = 100
  697. duration = 0
  698. }
  699. if tempTargetsArray.first?.active ?? false || booleanArray.first?.enabled ?? false {
  700. var duration_ = Int(truncating: tempTargetsArray.first?.duration ?? 0)
  701. var hbt = tempTargetsArray.first?.hbt ?? 160
  702. if booleanArray.first?.enabled ?? false, !(booleanArray.first?.isPreset ?? false) {
  703. duration_ = Int(truncating: booleanArray.first?.duration ?? 0)
  704. hbt = booleanArray.first?.hbt ?? 160
  705. }
  706. let startDate = tempTargetsArray.first?.startDate ?? Date()
  707. let durationPlusStart = startDate.addingTimeInterval(duration_.minutes.timeInterval)
  708. let dd = durationPlusStart.timeIntervalSinceNow.minutes
  709. if dd > 0 {
  710. hbtSetting = Decimal(hbt)
  711. isPercentageEnabled = true
  712. } else { isPercentageEnabled = false }
  713. }
  714. let averages = Oref2_variables(
  715. average_total_data: roundDecimal(average14, 1),
  716. weightedAverage: roundDecimal(weighted_average, 1),
  717. past2hoursAverage: roundDecimal(average2hours, 1),
  718. date: Date(),
  719. isEnabled: isPercentageEnabled,
  720. overridePercentage: overridePercentage,
  721. useOverride: useOverride,
  722. duration: duration,
  723. unlimited: unlimited,
  724. hbt: hbtSetting
  725. )
  726. storage.save(averages, as: OpenAPS.Monitor.oref2_variables)
  727. print("Test time of TDD: \(-1 * tddStartedAt.timeIntervalSinceNow) s")
  728. } else {
  729. coredataContext.performAndWait {
  730. let requestIsEnbled = TempTargetsSlider.fetchRequest() as NSFetchRequest<TempTargetsSlider>
  731. let sortIsEnabled = NSSortDescriptor(key: "date", ascending: false)
  732. requestIsEnbled.sortDescriptors = [sortIsEnabled]
  733. try? booleanArray = coredataContext.fetch(requestIsEnbled)
  734. let requestOverrides = Override.fetchRequest() as NSFetchRequest<Override>
  735. let sortOverride = NSSortDescriptor(key: "date", ascending: false)
  736. requestOverrides.sortDescriptors = [sortOverride]
  737. requestOverrides.fetchLimit = 1
  738. try? overrideArray = coredataContext.fetch(requestOverrides)
  739. let requestTempTargets = TempTargets.fetchRequest() as NSFetchRequest<TempTargets>
  740. let sortTT = NSSortDescriptor(key: "date", ascending: false)
  741. requestTempTargets.sortDescriptors = [sortTT]
  742. requestTempTargets.fetchLimit = 1
  743. try? tempTargetsArray = coredataContext.fetch(requestTempTargets)
  744. }
  745. isPercentageEnabled = booleanArray.first?.enabled ?? false
  746. useOverride = overrideArray.first?.enabled ?? false
  747. overridePercentage = Decimal(overrideArray.first?.percentage ?? 100)
  748. unlimited = overrideArray.first?.indefinite ?? true
  749. hbtSetting = Decimal(booleanArray.first?.hbt ?? 160)
  750. if useOverride {
  751. duration = (overrideArray.first?.duration ?? 0) as Decimal
  752. let addedMinutes = Int(duration)
  753. let date = overrideArray.first?.date ?? Date()
  754. if date.addingTimeInterval(addedMinutes.minutes.timeInterval) < Date(),
  755. !unlimited
  756. { useOverride = false }
  757. newDuration = Decimal(Date().distance(to: date.addingTimeInterval(addedMinutes.minutes.timeInterval)).minutes)
  758. }
  759. if newDuration < 0 {
  760. newDuration = 0
  761. } else { duration = newDuration }
  762. if !useOverride {
  763. unlimited = true
  764. overridePercentage = 100
  765. duration = 0
  766. }
  767. if tempTargetsArray.first?.active ?? false || booleanArray.first?.enabled ?? false {
  768. var duration_ = Int(truncating: tempTargetsArray.first?.duration ?? 0)
  769. var hbt = tempTargetsArray.first?.hbt ?? 160
  770. if booleanArray.first?.enabled ?? false, !(booleanArray.first?.isPreset ?? false) {
  771. duration_ = Int(truncating: booleanArray.first?.duration ?? 0)
  772. hbt = booleanArray.first?.hbt ?? 160
  773. }
  774. let startDate = tempTargetsArray.first?.startDate ?? Date()
  775. let durationPlusStart = startDate.addingTimeInterval(duration_.minutes.timeInterval)
  776. let dd = durationPlusStart.timeIntervalSinceNow.minutes
  777. if dd > 0 {
  778. hbtSetting = Decimal(hbt)
  779. isPercentageEnabled = true
  780. } else { isPercentageEnabled = false }
  781. }
  782. let averages = Oref2_variables(
  783. average_total_data: 0,
  784. weightedAverage: 1,
  785. past2hoursAverage: 0,
  786. date: Date(),
  787. isEnabled: isPercentageEnabled,
  788. overridePercentage: overridePercentage,
  789. useOverride: useOverride,
  790. duration: duration,
  791. unlimited: unlimited,
  792. hbt: hbtSetting
  793. )
  794. storage.save(averages, as: OpenAPS.Monitor.oref2_variables)
  795. }
  796. coredataContext.performAndWait {
  797. let saveNewUseOverride = Override(context: self.coredataContext)
  798. saveNewUseOverride.date = Date()
  799. saveNewUseOverride.enabled = useOverride
  800. saveNewUseOverride.percentage = Double(overridePercentage)
  801. saveNewUseOverride.duration = newDuration as NSDecimalNumber
  802. saveNewUseOverride.indefinite = unlimited
  803. try? self.coredataContext.save()
  804. }
  805. }
  806. private func roundDecimal(_ decimal: Decimal, _ digits: Double) -> Decimal {
  807. let rounded = round(Double(decimal) * pow(10, digits)) / pow(10, digits)
  808. return Decimal(rounded)
  809. }
  810. private func roundDouble(_ double: Double, _ digits: Double) -> Double {
  811. let rounded = round(Double(double) * pow(10, digits)) / pow(10, digits)
  812. return rounded
  813. }
  814. private func medianCalculation(array: [Double]) -> Double {
  815. guard !array.isEmpty else {
  816. return 0
  817. }
  818. let sorted = array.sorted()
  819. let length = array.count
  820. if length % 2 == 0 {
  821. return (sorted[length / 2 - 1] + sorted[length / 2]) / 2
  822. }
  823. return sorted[length / 2]
  824. }
  825. // Add to statistics.JSON
  826. private func statistics() {
  827. let statisticsStartedAt = Date()
  828. var testFile: [Statistics] = []
  829. var testIfEmpty = 0
  830. storage.transaction { storage in
  831. testFile = storage.retrieve(OpenAPS.Monitor.statistics, as: [Statistics].self) ?? []
  832. testIfEmpty = testFile.count
  833. }
  834. let updateThisOften = Int(settingsManager.preferences.updateInterval)
  835. // Only run every 30 minutes or according to setting.
  836. if testIfEmpty != 0 {
  837. guard testFile[0].created_at.addingTimeInterval(updateThisOften.minutes.timeInterval) < Date()
  838. else {
  839. return
  840. }
  841. }
  842. let units = settingsManager.settings.units
  843. let preferences = settingsManager.preferences
  844. // MARK: Fetch Carbs from CoreData
  845. var carbs = [Carbohydrates]()
  846. var carbTotal: Decimal = 0
  847. coredataContext.performAndWait {
  848. let requestCarbs = Carbohydrates.fetchRequest() as NSFetchRequest<Carbohydrates>
  849. let daysAgo = Date().addingTimeInterval(-1.days.timeInterval)
  850. requestCarbs.predicate = NSPredicate(format: "carbs > 0 AND date > %@", daysAgo as NSDate)
  851. let sortCarbs = NSSortDescriptor(key: "date", ascending: true)
  852. requestCarbs.sortDescriptors = [sortCarbs]
  853. try? carbs = coredataContext.fetch(requestCarbs)
  854. carbTotal = carbs.map({ carbs in carbs.carbs as? Decimal ?? 0 }).reduce(0, +)
  855. }
  856. // MARK: Fetch TDD from CoreData
  857. var tdds = [TDD]()
  858. var currentTDD: Decimal = 0
  859. coredataContext.performAndWait {
  860. let requestTDD = TDD.fetchRequest() as NSFetchRequest<TDD>
  861. let sort = NSSortDescriptor(key: "timestamp", ascending: false)
  862. requestTDD.sortDescriptors = [sort]
  863. requestTDD.fetchLimit = 1
  864. try? tdds = coredataContext.fetch(requestTDD)
  865. if !tdds.isEmpty {
  866. currentTDD = tdds[0].tdd?.decimalValue ?? 0
  867. }
  868. }
  869. var algo_ = "Oref0"
  870. if preferences.sigmoid, preferences.enableDynamicCR {
  871. algo_ = "Dynamic ISF + CR: Sigmoid"
  872. } else if preferences.sigmoid, !preferences.enableDynamicCR {
  873. algo_ = "Dynamic ISF: Sigmoid"
  874. } else if preferences.useNewFormula, preferences.enableDynamicCR {
  875. algo_ = "Dynamic ISF + CR: Logarithmic"
  876. } else if preferences.useNewFormula, !preferences.sigmoid,!preferences.enableDynamicCR {
  877. algo_ = "Dynamic ISF: Logarithmic"
  878. }
  879. let af = preferences.adjustmentFactor
  880. let insulin_type = preferences.curve
  881. let buildDate = Bundle.main.buildDate
  882. let version = Bundle.main.releaseVersionNumber
  883. let build = Bundle.main.buildVersionNumber
  884. let branch = Bundle.main.infoDictionary?["BuildBranch"] as? String ?? ""
  885. let copyrightNotice_ = Bundle.main.infoDictionary?["NSHumanReadableCopyright"] as? String ?? ""
  886. let pump_ = pumpManager?.localizedTitle ?? ""
  887. let cgm = settingsManager.settings.cgm
  888. let file = OpenAPS.Monitor.statistics
  889. var iPa: Decimal = 75
  890. if preferences.useCustomPeakTime {
  891. iPa = preferences.insulinPeakTime
  892. } else if preferences.curve.rawValue == "rapid-acting" {
  893. iPa = 65
  894. } else if preferences.curve.rawValue == "ultra-rapid" {
  895. iPa = 50
  896. }
  897. // MARK: Fetch LoopStatRecords from CoreData
  898. var lsr = [LoopStatRecord]()
  899. var successRate: Double?
  900. var successNR = 0
  901. var errorNR = 0
  902. var minimumInt = 999.0
  903. var maximumInt = 0.0
  904. var minimumLoopTime = 9999.0
  905. var maximumLoopTime = 0.0
  906. var timeIntervalLoops = 0.0
  907. var previousTimeLoop = Date()
  908. var timeForOneLoop = 0.0
  909. var averageLoopTime = 0.0
  910. var timeForOneLoopArray: [Double] = []
  911. var medianLoopTime = 0.0
  912. var timeIntervalLoopArray: [Double] = []
  913. var medianInterval = 0.0
  914. var averageIntervalLoops = 0.0
  915. var averageLoopDuration = 0.0
  916. coredataContext.performAndWait {
  917. let requestLSR = LoopStatRecord.fetchRequest() as NSFetchRequest<LoopStatRecord>
  918. requestLSR.predicate = NSPredicate(format: "start > %@", Date().addingTimeInterval(-24.hours.timeInterval) as NSDate)
  919. let sortLSR = NSSortDescriptor(key: "start", ascending: false)
  920. requestLSR.sortDescriptors = [sortLSR]
  921. try? lsr = coredataContext.fetch(requestLSR)
  922. if lsr.isNotEmpty {
  923. var i = 0.0
  924. if let loopEnd = lsr[0].end {
  925. previousTimeLoop = loopEnd
  926. }
  927. for each in lsr {
  928. if let loopEnd = each.end {
  929. let loopDuration = each.duration
  930. if each.loopStatus!.contains("Success") {
  931. successNR += 1
  932. } else {
  933. errorNR += 1
  934. }
  935. i += 1
  936. timeIntervalLoops = (previousTimeLoop - (each.start ?? previousTimeLoop)).timeInterval / 60
  937. if timeIntervalLoops > 0.0, i != 1 {
  938. timeIntervalLoopArray.append(timeIntervalLoops)
  939. }
  940. if timeIntervalLoops > maximumInt {
  941. maximumInt = timeIntervalLoops
  942. }
  943. if timeIntervalLoops < minimumInt, i != 1 {
  944. minimumInt = timeIntervalLoops
  945. }
  946. timeForOneLoop = loopDuration
  947. timeForOneLoopArray.append(timeForOneLoop)
  948. if timeForOneLoop >= maximumLoopTime, timeForOneLoop != 0.0 {
  949. maximumLoopTime = timeForOneLoop
  950. }
  951. if timeForOneLoop <= minimumLoopTime, timeForOneLoop != 0.0 {
  952. minimumLoopTime = timeForOneLoop
  953. }
  954. previousTimeLoop = loopEnd
  955. }
  956. }
  957. successRate = (Double(successNR) / Double(i)) * 100
  958. // Average Loop Interval in minutes
  959. let timeOfFirstIndex = lsr[0].start ?? Date()
  960. let lastIndexWithTimestamp = lsr.count - 1
  961. let timeOfLastIndex = lsr[lastIndexWithTimestamp].end ?? Date()
  962. averageLoopTime = (timeOfFirstIndex - timeOfLastIndex).timeInterval / 60 / Double(errorNR + successNR)
  963. // Median values
  964. medianLoopTime = medianCalculation(array: timeForOneLoopArray)
  965. medianInterval = medianCalculation(array: timeIntervalLoopArray)
  966. // Average time interval between loops
  967. averageIntervalLoops = timeIntervalLoopArray.reduce(0, +) / Double(timeIntervalLoopArray.count)
  968. // Average loop duration
  969. averageLoopDuration = timeForOneLoopArray.reduce(0, +) / Double(timeForOneLoopArray.count)
  970. }
  971. }
  972. if minimumInt == 999.0 {
  973. minimumInt = 0.0
  974. }
  975. if minimumLoopTime == 9999.0 {
  976. minimumLoopTime = 0.0
  977. }
  978. var glucose = [Readings]()
  979. var firstElementTime = Date()
  980. var lastElementTime = Date()
  981. var currentIndexTime = Date()
  982. var bg: Decimal = 0
  983. var bgArray: [Double] = []
  984. var bgArray_1_: [Double] = []
  985. var bgArray_7_: [Double] = []
  986. var bgArray_30_: [Double] = []
  987. var bgArray_90_: [Double] = []
  988. var bgArrayForTIR: [(bg_: Double, date_: Date)] = []
  989. var bgArray_1: [(bg_: Double, date_: Date)] = []
  990. var bgArray_7: [(bg_: Double, date_: Date)] = []
  991. var bgArray_30: [(bg_: Double, date_: Date)] = []
  992. var bgArray_90: [(bg_: Double, date_: Date)] = []
  993. var medianBG = 0.0
  994. var nr_bgs: Decimal = 0
  995. var bg_1: Decimal = 0
  996. var bg_7: Decimal = 0
  997. var bg_30: Decimal = 0
  998. var bg_90: Decimal = 0
  999. var bg_total: Decimal = 0
  1000. var j = -1
  1001. var conversionFactor: Decimal = 1
  1002. if units == .mmolL {
  1003. conversionFactor = 0.0555
  1004. }
  1005. var numberOfDays: Double = 0
  1006. var nr1: Decimal = 0
  1007. coredataContext.performAndWait {
  1008. let requestGFS = Readings.fetchRequest() as NSFetchRequest<Readings>
  1009. let sortGlucose = NSSortDescriptor(key: "date", ascending: false)
  1010. requestGFS.sortDescriptors = [sortGlucose]
  1011. try? glucose = coredataContext.fetch(requestGFS)
  1012. // Time In Range (%) and Average Glucose. This will be refactored later after some testing.
  1013. let endIndex = glucose.count - 1
  1014. firstElementTime = glucose[0].date ?? Date()
  1015. lastElementTime = glucose[endIndex].date ?? Date()
  1016. currentIndexTime = firstElementTime
  1017. numberOfDays = (firstElementTime - lastElementTime).timeInterval / 8.64E4
  1018. // Make arrays for median calculations and calculate averages
  1019. if endIndex >= 0 {
  1020. repeat {
  1021. j += 1
  1022. if glucose[j].glucose > 0 {
  1023. currentIndexTime = glucose[j].date ?? firstElementTime
  1024. bg += Decimal(glucose[j].glucose) * conversionFactor
  1025. bgArray.append(Double(glucose[j].glucose) * Double(conversionFactor))
  1026. bgArrayForTIR.append((Double(glucose[j].glucose), glucose[j].date!))
  1027. nr_bgs += 1
  1028. if (firstElementTime - currentIndexTime).timeInterval <= 8.64E4 { // 1 day
  1029. bg_1 = bg / nr_bgs
  1030. bgArray_1 = bgArrayForTIR
  1031. bgArray_1_ = bgArray
  1032. nr1 = nr_bgs
  1033. }
  1034. if (firstElementTime - currentIndexTime).timeInterval <= 6.048E5 { // 7 days
  1035. bg_7 = bg / nr_bgs
  1036. bgArray_7 = bgArrayForTIR
  1037. bgArray_7_ = bgArray
  1038. }
  1039. if (firstElementTime - currentIndexTime).timeInterval <= 2.592E6 { // 30 days
  1040. bg_30 = bg / nr_bgs
  1041. bgArray_30 = bgArrayForTIR
  1042. bgArray_30_ = bgArray
  1043. }
  1044. if (firstElementTime - currentIndexTime).timeInterval <= 7.776E7 { // 30 days
  1045. bg_90 = bg / nr_bgs
  1046. bgArray_90 = bgArrayForTIR
  1047. bgArray_90_ = bgArray
  1048. }
  1049. }
  1050. } while j != glucose.count - 1
  1051. }
  1052. }
  1053. if nr_bgs > 0 {
  1054. // Up to 91 days
  1055. bg_total = bg / nr_bgs
  1056. }
  1057. // Total median
  1058. medianBG = medianCalculation(array: bgArray)
  1059. func tir(_ array: [(bg_: Double, date_: Date)]) -> (TIR: Double, hypos: Double, hypers: Double) {
  1060. var timeInHypo = 0.0
  1061. var timeInHyper = 0.0
  1062. var hypos = 0.0
  1063. var hypers = 0.0
  1064. var i = -1
  1065. var lastIndex = false
  1066. let endIndex = array.count - 1
  1067. var hypoLimit = settingsManager.preferences.low
  1068. var hyperLimit = settingsManager.preferences.high
  1069. if units == .mmolL {
  1070. hypoLimit = hypoLimit / 0.0555
  1071. hyperLimit = hyperLimit / 0.0555
  1072. }
  1073. var full_time = 0.0
  1074. if endIndex > 0 {
  1075. full_time = (array[0].date_ - array[endIndex].date_).timeInterval
  1076. }
  1077. while i < endIndex {
  1078. i += 1
  1079. let currentTime = array[i].date_
  1080. var previousTime = currentTime
  1081. if i + 1 <= endIndex {
  1082. previousTime = array[i + 1].date_
  1083. } else {
  1084. lastIndex = true
  1085. }
  1086. if array[i].bg_ < Double(hypoLimit), !lastIndex {
  1087. timeInHypo += (currentTime - previousTime).timeInterval
  1088. } else if array[i].bg_ >= Double(hyperLimit), !lastIndex {
  1089. timeInHyper += (currentTime - previousTime).timeInterval
  1090. }
  1091. }
  1092. if timeInHypo == 0.0 {
  1093. hypos = 0
  1094. } else if full_time != 0.0 { hypos = (timeInHypo / full_time) * 100
  1095. }
  1096. if timeInHyper == 0.0 {
  1097. hypers = 0
  1098. } else if full_time != 0.0 { hypers = (timeInHyper / full_time) * 100
  1099. }
  1100. let TIR = 100 - (hypos + hypers)
  1101. return (roundDouble(TIR, 1), roundDouble(hypos, 1), roundDouble(hypers, 1))
  1102. }
  1103. // HbA1c estimation (%, mmol/mol) 1 day
  1104. var NGSPa1CStatisticValue: Decimal = 0.0
  1105. var IFCCa1CStatisticValue: Decimal = 0.0
  1106. if nr_bgs > 0 {
  1107. NGSPa1CStatisticValue = ((bg_1 / conversionFactor) + 46.7) / 28.7 // NGSP (%)
  1108. IFCCa1CStatisticValue = 10.929 *
  1109. (NGSPa1CStatisticValue - 2.152) // IFCC (mmol/mol) A1C(mmol/mol) = 10.929 * (A1C(%) - 2.15)
  1110. }
  1111. // 7 days
  1112. var NGSPa1CStatisticValue_7: Decimal = 0.0
  1113. var IFCCa1CStatisticValue_7: Decimal = 0.0
  1114. if nr_bgs > 0 {
  1115. NGSPa1CStatisticValue_7 = ((bg_7 / conversionFactor) + 46.7) / 28.7
  1116. IFCCa1CStatisticValue_7 = 10.929 * (NGSPa1CStatisticValue_7 - 2.152)
  1117. }
  1118. // 30 days
  1119. var NGSPa1CStatisticValue_30: Decimal = 0.0
  1120. var IFCCa1CStatisticValue_30: Decimal = 0.0
  1121. if nr_bgs > 0 {
  1122. NGSPa1CStatisticValue_30 = ((bg_30 / conversionFactor) + 46.7) / 28.7
  1123. IFCCa1CStatisticValue_30 = 10.929 * (NGSPa1CStatisticValue_30 - 2.152)
  1124. }
  1125. // 90 days
  1126. var NGSPa1CStatisticValue_90: Decimal = 0.0
  1127. var IFCCa1CStatisticValue_90: Decimal = 0.0
  1128. if nr_bgs > 0 {
  1129. NGSPa1CStatisticValue_90 = ((bg_90 / conversionFactor) + 46.7) / 28.7
  1130. IFCCa1CStatisticValue_90 = 10.929 * (NGSPa1CStatisticValue_90 - 2.152)
  1131. }
  1132. // Total days
  1133. var NGSPa1CStatisticValue_total: Decimal = 0.0
  1134. var IFCCa1CStatisticValue_total: Decimal = 0.0
  1135. if nr_bgs > 0 {
  1136. NGSPa1CStatisticValue_total = ((bg_total / conversionFactor) + 46.7) / 28.7
  1137. IFCCa1CStatisticValue_total = 10.929 *
  1138. (NGSPa1CStatisticValue_total - 2.152)
  1139. }
  1140. let median = Durations(
  1141. day: roundDecimal(Decimal(medianCalculation(array: bgArray_1_)), 1),
  1142. week: roundDecimal(Decimal(medianCalculation(array: bgArray_7_)), 1),
  1143. month: roundDecimal(Decimal(medianCalculation(array: bgArray_30_)), 1),
  1144. total: roundDecimal(Decimal(medianBG), 1)
  1145. )
  1146. // MARK: Save to Median to CoreData
  1147. coredataContext.perform {
  1148. let saveMedianToCoreData = BGmedian(context: self.coredataContext)
  1149. saveMedianToCoreData.date = Date()
  1150. saveMedianToCoreData.median = median.total as NSDecimalNumber
  1151. saveMedianToCoreData.median_1 = median.day as NSDecimalNumber
  1152. saveMedianToCoreData.median_7 = median.week as NSDecimalNumber
  1153. saveMedianToCoreData.median_30 = median.month as NSDecimalNumber
  1154. saveMedianToCoreData.median_90 = self.roundDecimal(
  1155. Decimal(self.medianCalculation(array: bgArray_90_)),
  1156. 1
  1157. ) as NSDecimalNumber
  1158. try? self.coredataContext.save()
  1159. }
  1160. var hbs = Durations(
  1161. day: roundDecimal(NGSPa1CStatisticValue, 1),
  1162. week: roundDecimal(NGSPa1CStatisticValue_7, 1),
  1163. month: roundDecimal(NGSPa1CStatisticValue_30, 1),
  1164. total: roundDecimal(NGSPa1CStatisticValue_total, 1)
  1165. )
  1166. coredataContext.perform {
  1167. let saveHbA1c = HbA1c(context: self.coredataContext)
  1168. saveHbA1c.date = Date()
  1169. saveHbA1c.hba1c = NGSPa1CStatisticValue_total as NSDecimalNumber
  1170. saveHbA1c.hba1c_1 = NGSPa1CStatisticValue as NSDecimalNumber
  1171. saveHbA1c.hba1c_7 = NGSPa1CStatisticValue_7 as NSDecimalNumber
  1172. saveHbA1c.hba1c_30 = NGSPa1CStatisticValue_30 as NSDecimalNumber
  1173. saveHbA1c.hba1c_90 = NGSPa1CStatisticValue_90 as NSDecimalNumber
  1174. try? self.coredataContext.save()
  1175. }
  1176. // Convert to user-preferred unit
  1177. let overrideHbA1cUnit = settingsManager.preferences.overrideHbA1cUnit
  1178. if units == .mmolL {
  1179. // Override if users sets overrideHbA1cUnit: true
  1180. if !overrideHbA1cUnit {
  1181. hbs = Durations(
  1182. day: roundDecimal(IFCCa1CStatisticValue, 1),
  1183. week: roundDecimal(IFCCa1CStatisticValue_7, 1),
  1184. month: roundDecimal(IFCCa1CStatisticValue_30, 1),
  1185. total: roundDecimal(IFCCa1CStatisticValue_total, 1)
  1186. )
  1187. }
  1188. } else if units != .mmolL, overrideHbA1cUnit {
  1189. hbs = Durations(
  1190. day: roundDecimal(IFCCa1CStatisticValue, 1),
  1191. week: roundDecimal(IFCCa1CStatisticValue_7, 1),
  1192. month: roundDecimal(IFCCa1CStatisticValue_30, 1),
  1193. total: roundDecimal(IFCCa1CStatisticValue_total, 1)
  1194. )
  1195. }
  1196. let nrOfCGMReadings = nr1
  1197. let loopstat = LoopCycles(
  1198. loops: successNR + errorNR,
  1199. errors: errorNR,
  1200. readings: Int(nrOfCGMReadings),
  1201. success_rate: Decimal(round(successRate ?? 0)),
  1202. avg_interval: roundDecimal(Decimal(averageLoopTime), 1),
  1203. median_interval: roundDecimal(Decimal(medianInterval), 1),
  1204. min_interval: roundDecimal(Decimal(minimumInt), 1),
  1205. max_interval: roundDecimal(Decimal(maximumInt), 1),
  1206. avg_duration: Decimal(roundDouble(averageLoopDuration, 2)),
  1207. median_duration: Decimal(roundDouble(medianLoopTime, 2)),
  1208. min_duration: roundDecimal(Decimal(minimumLoopTime), 2),
  1209. max_duration: Decimal(roundDouble(maximumLoopTime, 1))
  1210. )
  1211. // TIR calcs for every case
  1212. var oneDay_: (TIR: Double, hypos: Double, hypers: Double) = (0.0, 0.0, 0.0)
  1213. var sevenDays_: (TIR: Double, hypos: Double, hypers: Double) = (0.0, 0.0, 0.0)
  1214. var thirtyDays_: (TIR: Double, hypos: Double, hypers: Double) = (0.0, 0.0, 0.0)
  1215. var totalDays_: (TIR: Double, hypos: Double, hypers: Double) = (0.0, 0.0, 0.0)
  1216. // Get all TIR calcs for every case
  1217. if nr_bgs > 0 {
  1218. oneDay_ = tir(bgArray_1)
  1219. sevenDays_ = tir(bgArray_7)
  1220. thirtyDays_ = tir(bgArray_30)
  1221. totalDays_ = tir(bgArrayForTIR)
  1222. }
  1223. let tir = Durations(
  1224. day: roundDecimal(Decimal(oneDay_.TIR), 1),
  1225. week: roundDecimal(Decimal(sevenDays_.TIR), 1),
  1226. month: roundDecimal(Decimal(thirtyDays_.TIR), 1),
  1227. total: roundDecimal(Decimal(totalDays_.TIR), 1)
  1228. )
  1229. let hypo = Durations(
  1230. day: Decimal(oneDay_.hypos),
  1231. week: Decimal(sevenDays_.hypos),
  1232. month: Decimal(thirtyDays_.hypos),
  1233. total: Decimal(totalDays_.hypos)
  1234. )
  1235. let hyper = Durations(
  1236. day: Decimal(oneDay_.hypers),
  1237. week: Decimal(sevenDays_.hypers),
  1238. month: Decimal(thirtyDays_.hypers),
  1239. total: Decimal(totalDays_.hypers)
  1240. )
  1241. let TimeInRange = TIRs(TIR: tir, Hypos: hypo, Hypers: hyper)
  1242. let avgs = Durations(
  1243. day: roundDecimal(bg_1, 1),
  1244. week: roundDecimal(bg_7, 1),
  1245. month: roundDecimal(bg_30, 1),
  1246. total: roundDecimal(bg_total, 1)
  1247. )
  1248. coredataContext.perform {
  1249. let saveAverages = BGaverages(context: self.coredataContext)
  1250. saveAverages.date = Date()
  1251. saveAverages.average = bg_total as NSDecimalNumber
  1252. saveAverages.average_1 = bg_1 as NSDecimalNumber
  1253. saveAverages.average_7 = bg_7 as NSDecimalNumber
  1254. saveAverages.average_30 = bg_30 as NSDecimalNumber
  1255. saveAverages.average_90 = bg_90 as NSDecimalNumber
  1256. try? self.coredataContext.save()
  1257. }
  1258. let avg = Averages(Average: avgs, Median: median)
  1259. var insulinDistribution = [InsulinDistribution]()
  1260. var insulin = Ins(
  1261. TDD: 0,
  1262. bolus: 0,
  1263. temp_basal: 0,
  1264. scheduled_basal: 0
  1265. )
  1266. coredataContext.performAndWait {
  1267. let requestInsulinDistribution = InsulinDistribution.fetchRequest() as NSFetchRequest<InsulinDistribution>
  1268. let sortInsulin = NSSortDescriptor(key: "date", ascending: false)
  1269. requestInsulinDistribution.sortDescriptors = [sortInsulin]
  1270. requestInsulinDistribution.fetchLimit = 1
  1271. try? insulinDistribution = coredataContext.fetch(requestInsulinDistribution)
  1272. insulin = Ins(
  1273. TDD: roundDecimal(currentTDD, 2),
  1274. bolus: insulinDistribution.first != nil ? ((insulinDistribution[0].bolus ?? 0) as Decimal) : 0,
  1275. temp_basal: insulinDistribution.first != nil ? ((insulinDistribution[0].tempBasal ?? 0) as Decimal) : 0,
  1276. scheduled_basal: insulinDistribution.first != nil ? ((insulinDistribution[0].scheduledBasal ?? 0) as Decimal) : 0
  1277. )
  1278. }
  1279. var sumOfSquares = 0.0
  1280. var sumOfSquares_1 = 0.0
  1281. var sumOfSquares_7 = 0.0
  1282. var sumOfSquares_30 = 0.0
  1283. // Total
  1284. for array in bgArray {
  1285. sumOfSquares += pow(array - Double(bg_total), 2)
  1286. }
  1287. // One day
  1288. for array_1 in bgArray_1_ {
  1289. sumOfSquares_1 += pow(array_1 - Double(bg_1), 2)
  1290. }
  1291. // week
  1292. for array_7 in bgArray_7_ {
  1293. sumOfSquares_7 += pow(array_7 - Double(bg_7), 2)
  1294. }
  1295. // month
  1296. for array_30 in bgArray_30_ {
  1297. sumOfSquares_30 += pow(array_30 - Double(bg_30), 2)
  1298. }
  1299. // Standard deviation and Coefficient of variation
  1300. var sd_total = 0.0
  1301. var cv_total = 0.0
  1302. var sd_1 = 0.0
  1303. var cv_1 = 0.0
  1304. var sd_7 = 0.0
  1305. var cv_7 = 0.0
  1306. var sd_30 = 0.0
  1307. var cv_30 = 0.0
  1308. // Avoid division by zero
  1309. if bg_total > 0 {
  1310. sd_total = sqrt(sumOfSquares / Double(nr_bgs))
  1311. cv_total = sd_total / Double(bg_total) * 100
  1312. }
  1313. if bg_1 > 0 {
  1314. sd_1 = sqrt(sumOfSquares_1 / Double(bgArray_1_.count))
  1315. cv_1 = sd_1 / Double(bg_1) * 100
  1316. }
  1317. if bg_7 > 0 {
  1318. sd_7 = sqrt(sumOfSquares_7 / Double(bgArray_7_.count))
  1319. cv_7 = sd_7 / Double(bg_7) * 100
  1320. }
  1321. if bg_30 > 0 {
  1322. sd_30 = sqrt(sumOfSquares_30 / Double(bgArray_30_.count))
  1323. cv_30 = sd_30 / Double(bg_30) * 100
  1324. }
  1325. // Standard Deviations
  1326. let standardDeviations = Durations(
  1327. day: roundDecimal(Decimal(sd_1), 1),
  1328. week: roundDecimal(Decimal(sd_7), 1),
  1329. month: roundDecimal(Decimal(sd_30), 1),
  1330. total: roundDecimal(Decimal(sd_total), 1)
  1331. )
  1332. // CV = standard deviation / sample mean x 100
  1333. let cvs = Durations(
  1334. day: roundDecimal(Decimal(cv_1), 1),
  1335. week: roundDecimal(Decimal(cv_7), 1),
  1336. month: roundDecimal(Decimal(cv_30), 1),
  1337. total: roundDecimal(Decimal(cv_total), 1)
  1338. )
  1339. let variance = Variance(SD: standardDeviations, CV: cvs)
  1340. let dailystat = Statistics(
  1341. created_at: Date(),
  1342. iPhone: UIDevice.current.getDeviceId,
  1343. iOS: UIDevice.current.getOSInfo,
  1344. Build_Version: version ?? "",
  1345. Build_Number: build ?? "1",
  1346. Branch: branch,
  1347. CopyRightNotice: String(copyrightNotice_.prefix(32)),
  1348. Build_Date: buildDate,
  1349. Algorithm: algo_,
  1350. AdjustmentFactor: af,
  1351. Pump: pump_,
  1352. CGM: cgm.rawValue,
  1353. insulinType: insulin_type.rawValue,
  1354. peakActivityTime: iPa,
  1355. Carbs_24h: carbTotal,
  1356. GlucoseStorage_Days: Decimal(roundDouble(numberOfDays, 1)),
  1357. Statistics: Stats(
  1358. Distribution: TimeInRange,
  1359. Glucose: avg,
  1360. HbA1c: hbs,
  1361. LoopCycles: loopstat,
  1362. Insulin: insulin,
  1363. Variance: variance
  1364. )
  1365. )
  1366. storage.transaction { storage in
  1367. storage.append(dailystat, to: file, uniqBy: \.created_at)
  1368. let uniqeEvents: [Statistics] = storage.retrieve(file, as: [Statistics].self)?
  1369. .filter { $0.created_at.addingTimeInterval(24.hours.timeInterval) > Date() }
  1370. .sorted { $0.created_at > $1.created_at } ?? []
  1371. storage.save(Array(uniqeEvents), as: file)
  1372. }
  1373. nightscout.uploadStatistics(dailystat: dailystat)
  1374. nightscout.uploadPreferences()
  1375. print("Test time of statistics computation: \(-1 * statisticsStartedAt.timeIntervalSinceNow) s")
  1376. }
  1377. private func loopStats(loopStatRecord: LoopStats) {
  1378. let LoopStatsStartedAt = Date()
  1379. coredataContext.perform {
  1380. let nLS = LoopStatRecord(context: self.coredataContext)
  1381. nLS.start = loopStatRecord.start
  1382. nLS.end = loopStatRecord.end ?? Date()
  1383. nLS.loopStatus = loopStatRecord.loopStatus
  1384. nLS.duration = loopStatRecord.duration ?? 0.0
  1385. try? self.coredataContext.save()
  1386. }
  1387. print("Test time of LoopStats computation: \(-1 * LoopStatsStartedAt.timeIntervalSinceNow) s")
  1388. }
  1389. private func processError(_ error: Error) {
  1390. warning(.apsManager, "\(error.localizedDescription)")
  1391. lastError.send(error)
  1392. }
  1393. private func createBolusReporter() {
  1394. bolusReporter = pumpManager?.createBolusProgressReporter(reportingOn: processQueue)
  1395. bolusReporter?.addObserver(self)
  1396. }
  1397. private func updateStatus() {
  1398. debug(.apsManager, "force update status")
  1399. guard let pump = pumpManager else {
  1400. return
  1401. }
  1402. if let omnipod = pump as? OmnipodPumpManager {
  1403. omnipod.getPodStatus { _ in }
  1404. }
  1405. if let omnipodBLE = pump as? OmniBLEPumpManager {
  1406. omnipodBLE.getPodStatus { _ in }
  1407. }
  1408. }
  1409. private func clearBolusReporter() {
  1410. bolusReporter?.removeObserver(self)
  1411. bolusReporter = nil
  1412. processQueue.asyncAfter(deadline: .now() + 0.5) {
  1413. self.bolusProgress.send(nil)
  1414. self.updateStatus()
  1415. }
  1416. }
  1417. }
  1418. private extension PumpManager {
  1419. func enactTempBasal(unitsPerHour: Double, for duration: TimeInterval) -> AnyPublisher<DoseEntry?, Error> {
  1420. Future { promise in
  1421. self.enactTempBasal(unitsPerHour: unitsPerHour, for: duration) { error in
  1422. if let error = error {
  1423. debug(.apsManager, "Temp basal failed: \(unitsPerHour) for: \(duration)")
  1424. promise(.failure(error))
  1425. } else {
  1426. debug(.apsManager, "Temp basal succeded: \(unitsPerHour) for: \(duration)")
  1427. promise(.success(nil))
  1428. }
  1429. }
  1430. }
  1431. .mapError { APSError.pumpError($0) }
  1432. .eraseToAnyPublisher()
  1433. }
  1434. func enactBolus(units: Double, automatic: Bool) -> AnyPublisher<DoseEntry?, Error> {
  1435. Future { promise in
  1436. // convert automatic
  1437. let automaticValue = automatic ? BolusActivationType.automatic : BolusActivationType.manualRecommendationAccepted
  1438. self.enactBolus(units: units, activationType: automaticValue) { error in
  1439. if let error = error {
  1440. debug(.apsManager, "Bolus failed: \(units)")
  1441. promise(.failure(error))
  1442. } else {
  1443. debug(.apsManager, "Bolus succeded: \(units)")
  1444. promise(.success(nil))
  1445. }
  1446. }
  1447. }
  1448. .mapError { APSError.pumpError($0) }
  1449. .eraseToAnyPublisher()
  1450. }
  1451. func cancelBolus() -> AnyPublisher<DoseEntry?, Error> {
  1452. Future { promise in
  1453. self.cancelBolus { result in
  1454. switch result {
  1455. case let .success(dose):
  1456. debug(.apsManager, "Cancel Bolus succeded")
  1457. promise(.success(dose))
  1458. case let .failure(error):
  1459. debug(.apsManager, "Cancel Bolus failed")
  1460. promise(.failure(error))
  1461. }
  1462. }
  1463. }
  1464. .mapError { APSError.pumpError($0) }
  1465. .eraseToAnyPublisher()
  1466. }
  1467. func suspendDelivery() -> AnyPublisher<Void, Error> {
  1468. Future { promise in
  1469. self.suspendDelivery { error in
  1470. if let error = error {
  1471. promise(.failure(error))
  1472. } else {
  1473. promise(.success(()))
  1474. }
  1475. }
  1476. }
  1477. .mapError { APSError.pumpError($0) }
  1478. .eraseToAnyPublisher()
  1479. }
  1480. func resumeDelivery() -> AnyPublisher<Void, Error> {
  1481. Future { promise in
  1482. self.resumeDelivery { error in
  1483. if let error = error {
  1484. promise(.failure(error))
  1485. } else {
  1486. promise(.success(()))
  1487. }
  1488. }
  1489. }
  1490. .mapError { APSError.pumpError($0) }
  1491. .eraseToAnyPublisher()
  1492. }
  1493. }
  1494. extension BaseAPSManager: PumpManagerStatusObserver {
  1495. func pumpManager(_: PumpManager, didUpdate status: PumpManagerStatus, oldStatus _: PumpManagerStatus) {
  1496. let percent = Int((status.pumpBatteryChargeRemaining ?? 1) * 100)
  1497. let battery = Battery(
  1498. percent: percent,
  1499. voltage: nil,
  1500. string: percent > 10 ? .normal : .low,
  1501. display: status.pumpBatteryChargeRemaining != nil
  1502. )
  1503. storage.save(battery, as: OpenAPS.Monitor.battery)
  1504. storage.save(status.pumpStatus, as: OpenAPS.Monitor.status)
  1505. }
  1506. }
  1507. extension BaseAPSManager: DoseProgressObserver {
  1508. func doseProgressReporterDidUpdate(_ doseProgressReporter: DoseProgressReporter) {
  1509. bolusProgress.send(Decimal(doseProgressReporter.progress.percentComplete))
  1510. if doseProgressReporter.progress.isComplete {
  1511. clearBolusReporter()
  1512. }
  1513. }
  1514. }
  1515. extension PumpManagerStatus {
  1516. var pumpStatus: PumpStatus {
  1517. let bolusing = bolusState != .noBolus
  1518. let suspended = basalDeliveryState?.isSuspended ?? true
  1519. let type = suspended ? StatusType.suspended : (bolusing ? .bolusing : .normal)
  1520. return PumpStatus(status: type, bolusing: bolusing, suspended: suspended, timestamp: Date())
  1521. }
  1522. }