APSManager.swift 43 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175
  1. import Combine
  2. import Foundation
  3. import LoopKit
  4. import LoopKitUI
  5. import OmniBLE
  6. import OmniKit
  7. import RileyLinkKit
  8. import SwiftDate
  9. import Swinject
  10. protocol APSManager {
  11. func heartbeat(date: Date)
  12. func autotune() -> AnyPublisher<Autotune?, Never>
  13. func enactBolus(amount: Double, isSMB: Bool)
  14. var pumpManager: PumpManagerUI? { get set }
  15. var bluetoothManager: BluetoothStateManager? { get }
  16. var pumpDisplayState: CurrentValueSubject<PumpDisplayState?, Never> { get }
  17. var pumpName: CurrentValueSubject<String, Never> { get }
  18. var isLooping: CurrentValueSubject<Bool, Never> { get }
  19. var lastLoopDate: Date { get }
  20. var lastLoopDateSubject: PassthroughSubject<Date, Never> { get }
  21. var bolusProgress: CurrentValueSubject<Decimal?, Never> { get }
  22. var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> { get }
  23. var isManualTempBasal: Bool { get }
  24. func enactTempBasal(rate: Double, duration: TimeInterval)
  25. func makeProfiles() -> AnyPublisher<Bool, Never>
  26. func determineBasal() -> AnyPublisher<Bool, Never>
  27. func determineBasalSync()
  28. func roundBolus(amount: Decimal) -> Decimal
  29. var lastError: CurrentValueSubject<Error?, Never> { get }
  30. func cancelBolus()
  31. func enactAnnouncement(_ announcement: Announcement)
  32. }
  33. enum APSError: LocalizedError {
  34. case pumpError(Error)
  35. case invalidPumpState(message: String)
  36. case glucoseError(message: String)
  37. case apsError(message: String)
  38. case deviceSyncError(message: String)
  39. case manualBasalTemp(message: String)
  40. var errorDescription: String? {
  41. switch self {
  42. case let .pumpError(error):
  43. return "Pump error: \(error.localizedDescription)"
  44. case let .invalidPumpState(message):
  45. return "Error: Invalid Pump State: \(message)"
  46. case let .glucoseError(message):
  47. return "Error: Invalid glucose: \(message)"
  48. case let .apsError(message):
  49. return "APS error: \(message)"
  50. case let .deviceSyncError(message):
  51. return "Sync error: \(message)"
  52. case let .manualBasalTemp(message):
  53. return "Manual Basal Temp : \(message)"
  54. }
  55. }
  56. }
  57. final class BaseAPSManager: APSManager, Injectable {
  58. private let processQueue = DispatchQueue(label: "BaseAPSManager.processQueue")
  59. @Injected() private var storage: FileStorage!
  60. @Injected() private var pumpHistoryStorage: PumpHistoryStorage!
  61. @Injected() private var alertHistoryStorage: AlertHistoryStorage!
  62. @Injected() private var glucoseStorage: GlucoseStorage!
  63. @Injected() private var tempTargetsStorage: TempTargetsStorage!
  64. @Injected() private var carbsStorage: CarbsStorage!
  65. @Injected() private var announcementsStorage: AnnouncementsStorage!
  66. @Injected() private var deviceDataManager: DeviceDataManager!
  67. @Injected() private var nightscout: NightscoutManager!
  68. @Injected() private var settingsManager: SettingsManager!
  69. @Injected() private var broadcaster: Broadcaster!
  70. @Persisted(key: "lastAutotuneDate") private var lastAutotuneDate = Date()
  71. @Persisted(key: "lastLoopDate") var lastLoopDate: Date = .distantPast {
  72. didSet {
  73. lastLoopDateSubject.send(lastLoopDate)
  74. }
  75. }
  76. private var openAPS: OpenAPS!
  77. private var lifetime = Lifetime()
  78. var pumpManager: PumpManagerUI? {
  79. get { deviceDataManager.pumpManager }
  80. set { deviceDataManager.pumpManager = newValue }
  81. }
  82. var bluetoothManager: BluetoothStateManager? { deviceDataManager.bluetoothManager }
  83. @Persisted(key: "isManualTempBasal") var isManualTempBasal: Bool = false
  84. let isLooping = CurrentValueSubject<Bool, Never>(false)
  85. let lastLoopDateSubject = PassthroughSubject<Date, Never>()
  86. let lastError = CurrentValueSubject<Error?, Never>(nil)
  87. let bolusProgress = CurrentValueSubject<Decimal?, Never>(nil)
  88. var pumpDisplayState: CurrentValueSubject<PumpDisplayState?, Never> {
  89. deviceDataManager.pumpDisplayState
  90. }
  91. var pumpName: CurrentValueSubject<String, Never> {
  92. deviceDataManager.pumpName
  93. }
  94. var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> {
  95. deviceDataManager.pumpExpiresAtDate
  96. }
  97. var settings: FreeAPSSettings {
  98. get { settingsManager.settings }
  99. set { settingsManager.settings = newValue }
  100. }
  101. init(resolver: Resolver) {
  102. injectServices(resolver)
  103. openAPS = OpenAPS(storage: storage)
  104. subscribe()
  105. lastLoopDateSubject.send(lastLoopDate)
  106. isLooping
  107. .weakAssign(to: \.deviceDataManager.loopInProgress, on: self)
  108. .store(in: &lifetime)
  109. }
  110. private func subscribe() {
  111. deviceDataManager.recommendsLoop
  112. .receive(on: processQueue)
  113. .sink { [weak self] in
  114. self?.loop()
  115. }
  116. .store(in: &lifetime)
  117. pumpManager?.addStatusObserver(self, queue: processQueue)
  118. deviceDataManager.errorSubject
  119. .receive(on: processQueue)
  120. .map { APSError.pumpError($0) }
  121. .sink {
  122. self.processError($0)
  123. }
  124. .store(in: &lifetime)
  125. deviceDataManager.bolusTrigger
  126. .receive(on: processQueue)
  127. .sink { bolusing in
  128. if bolusing {
  129. self.createBolusReporter()
  130. } else {
  131. self.clearBolusReporter()
  132. }
  133. }
  134. .store(in: &lifetime)
  135. // manage a manual Temp Basal from OmniPod - Force loop() after stop a temp basal or finished
  136. deviceDataManager.manualTempBasal
  137. .receive(on: processQueue)
  138. .sink { manualBasal in
  139. if manualBasal {
  140. self.isManualTempBasal = true
  141. } else {
  142. if self.isManualTempBasal {
  143. self.isManualTempBasal = false
  144. self.loop()
  145. }
  146. }
  147. }
  148. .store(in: &lifetime)
  149. }
  150. func heartbeat(date: Date) {
  151. deviceDataManager.heartbeat(date: date)
  152. }
  153. // Loop entry point
  154. private func loop() {
  155. guard !isLooping.value else {
  156. warning(.apsManager, "Already looping, skip")
  157. return
  158. }
  159. debug(.apsManager, "Starting loop")
  160. isLooping.send(true)
  161. determineBasal()
  162. .replaceEmpty(with: false)
  163. .flatMap { [weak self] success -> AnyPublisher<Void, Error> in
  164. guard let self = self, success else {
  165. return Fail(error: APSError.apsError(message: "Determine basal failed")).eraseToAnyPublisher()
  166. }
  167. // Open loop completed
  168. guard self.settings.closedLoop else {
  169. return Just(()).setFailureType(to: Error.self).eraseToAnyPublisher()
  170. }
  171. self.nightscout.uploadStatus()
  172. // Closed loop - enact suggested
  173. return self.enactSuggested()
  174. }
  175. .sink { [weak self] completion in
  176. guard let self = self else { return }
  177. if case let .failure(error) = completion {
  178. self.loopCompleted(error: error)
  179. } else {
  180. self.loopCompleted()
  181. }
  182. } receiveValue: {}
  183. .store(in: &lifetime)
  184. }
  185. // Loop exit point
  186. private func loopCompleted(error: Error? = nil) {
  187. isLooping.send(false)
  188. if let error = error {
  189. warning(.apsManager, "Loop failed with error: \(error.localizedDescription)")
  190. processError(error)
  191. } else {
  192. debug(.apsManager, "Loop succeeded")
  193. lastLoopDate = Date()
  194. lastError.send(nil)
  195. }
  196. if settings.closedLoop {
  197. reportEnacted(received: error == nil)
  198. }
  199. }
  200. private func verifyStatus() -> Error? {
  201. guard let pump = pumpManager else {
  202. return APSError.invalidPumpState(message: "Pump not set")
  203. }
  204. let status = pump.status.pumpStatus
  205. guard !status.bolusing else {
  206. return APSError.invalidPumpState(message: "Pump is bolusing")
  207. }
  208. guard !status.suspended else {
  209. return APSError.invalidPumpState(message: "Pump suspended")
  210. }
  211. let reservoir = storage.retrieve(OpenAPS.Monitor.reservoir, as: Decimal.self) ?? 100
  212. guard reservoir >= 0 else {
  213. return APSError.invalidPumpState(message: "Reservoir is empty")
  214. }
  215. return nil
  216. }
  217. private func autosens() -> AnyPublisher<Bool, Never> {
  218. guard let autosens = storage.retrieve(OpenAPS.Settings.autosense, as: Autosens.self),
  219. (autosens.timestamp ?? .distantPast).addingTimeInterval(30.minutes.timeInterval) > Date()
  220. else {
  221. return openAPS.autosense()
  222. .map { $0 != nil }
  223. .eraseToAnyPublisher()
  224. }
  225. return Just(false).eraseToAnyPublisher()
  226. }
  227. func determineBasal() -> AnyPublisher<Bool, Never> {
  228. debug(.apsManager, "Start determine basal")
  229. guard let glucose = storage.retrieve(OpenAPS.Monitor.glucose, as: [BloodGlucose].self), glucose.isNotEmpty else {
  230. debug(.apsManager, "Not enough glucose data")
  231. processError(APSError.glucoseError(message: "Not enough glucose data"))
  232. return Just(false).eraseToAnyPublisher()
  233. }
  234. let lastGlucoseDate = glucoseStorage.lastGlucoseDate()
  235. guard lastGlucoseDate >= Date().addingTimeInterval(-12.minutes.timeInterval) else {
  236. debug(.apsManager, "Glucose data is stale")
  237. processError(APSError.glucoseError(message: "Glucose data is stale"))
  238. return Just(false).eraseToAnyPublisher()
  239. }
  240. guard glucoseStorage.isGlucoseNotFlat() else {
  241. debug(.apsManager, "Glucose data is too flat")
  242. processError(APSError.glucoseError(message: "Glucose data is too flat"))
  243. return Just(false).eraseToAnyPublisher()
  244. }
  245. let now = Date()
  246. let temp = currentTemp(date: now)
  247. let mainPublisher = makeProfiles()
  248. .flatMap { _ in self.autosens() }
  249. .flatMap { _ in self.dailyAutotune() }
  250. .flatMap { _ in self.openAPS.determineBasal(currentTemp: temp, clock: now) }
  251. .map { suggestion -> Bool in
  252. if let suggestion = suggestion {
  253. DispatchQueue.main.async {
  254. self.broadcaster.notify(SuggestionObserver.self, on: .main) {
  255. $0.suggestionDidUpdate(suggestion)
  256. }
  257. }
  258. }
  259. return suggestion != nil
  260. }
  261. .eraseToAnyPublisher()
  262. if temp.duration == 0,
  263. settings.closedLoop,
  264. settingsManager.preferences.unsuspendIfNoTemp,
  265. let pump = pumpManager,
  266. pump.status.pumpStatus.suspended
  267. {
  268. return pump.resumeDelivery()
  269. .flatMap { _ in mainPublisher }
  270. .replaceError(with: false)
  271. .eraseToAnyPublisher()
  272. }
  273. return mainPublisher
  274. }
  275. func determineBasalSync() {
  276. determineBasal().cancellable().store(in: &lifetime)
  277. }
  278. func makeProfiles() -> AnyPublisher<Bool, Never> {
  279. openAPS.makeProfiles(useAutotune: settings.useAutotune)
  280. .map { tunedProfile in
  281. if let basalProfile = tunedProfile?.basalProfile {
  282. self.processQueue.async {
  283. self.broadcaster.notify(BasalProfileObserver.self, on: self.processQueue) {
  284. $0.basalProfileDidChange(basalProfile)
  285. }
  286. }
  287. }
  288. return tunedProfile != nil
  289. }
  290. .eraseToAnyPublisher()
  291. }
  292. func roundBolus(amount: Decimal) -> Decimal {
  293. guard let pump = pumpManager else { return amount }
  294. let rounded = Decimal(pump.roundToSupportedBolusVolume(units: Double(amount)))
  295. let maxBolus = Decimal(pump.roundToSupportedBolusVolume(units: Double(settingsManager.pumpSettings.maxBolus)))
  296. return min(rounded, maxBolus)
  297. }
  298. private var bolusReporter: DoseProgressReporter?
  299. func enactBolus(amount: Double, isSMB: Bool) {
  300. if let error = verifyStatus() {
  301. processError(error)
  302. processQueue.async {
  303. self.broadcaster.notify(BolusFailureObserver.self, on: self.processQueue) {
  304. $0.bolusDidFail()
  305. }
  306. }
  307. return
  308. }
  309. guard let pump = pumpManager else { return }
  310. let roundedAmout = pump.roundToSupportedBolusVolume(units: amount)
  311. debug(.apsManager, "Enact bolus \(roundedAmout), manual \(!isSMB)")
  312. pump.enactBolus(units: roundedAmout, automatic: isSMB).sink { completion in
  313. if case let .failure(error) = completion {
  314. warning(.apsManager, "Bolus failed with error: \(error.localizedDescription)")
  315. self.processError(APSError.pumpError(error))
  316. if !isSMB {
  317. self.processQueue.async {
  318. self.broadcaster.notify(BolusFailureObserver.self, on: self.processQueue) {
  319. $0.bolusDidFail()
  320. }
  321. }
  322. }
  323. } else {
  324. debug(.apsManager, "Bolus succeeded")
  325. if !isSMB {
  326. self.determineBasal().sink { _ in }.store(in: &self.lifetime)
  327. }
  328. self.bolusProgress.send(0)
  329. }
  330. } receiveValue: { _ in }
  331. .store(in: &lifetime)
  332. }
  333. func cancelBolus() {
  334. guard let pump = pumpManager, pump.status.pumpStatus.bolusing else { return }
  335. debug(.apsManager, "Cancel bolus")
  336. pump.cancelBolus().sink { completion in
  337. if case let .failure(error) = completion {
  338. debug(.apsManager, "Bolus cancellation failed with error: \(error.localizedDescription)")
  339. self.processError(APSError.pumpError(error))
  340. } else {
  341. debug(.apsManager, "Bolus cancelled")
  342. }
  343. self.bolusReporter?.removeObserver(self)
  344. self.bolusReporter = nil
  345. self.bolusProgress.send(nil)
  346. } receiveValue: { _ in }
  347. .store(in: &lifetime)
  348. }
  349. func enactTempBasal(rate: Double, duration: TimeInterval) {
  350. if let error = verifyStatus() {
  351. processError(error)
  352. return
  353. }
  354. guard let pump = pumpManager else { return }
  355. // unable to do temp basal during manual temp basal 😁
  356. if isManualTempBasal {
  357. processError(APSError.manualBasalTemp(message: "Loop not possible during the manual basal temp"))
  358. return
  359. }
  360. debug(.apsManager, "Enact temp basal \(rate) - \(duration)")
  361. let roundedAmout = pump.roundToSupportedBasalRate(unitsPerHour: rate)
  362. pump.enactTempBasal(unitsPerHour: roundedAmout, for: duration) { error in
  363. if let error = error {
  364. debug(.apsManager, "Temp Basal failed with error: \(error.localizedDescription)")
  365. self.processError(APSError.pumpError(error))
  366. } else {
  367. debug(.apsManager, "Temp Basal succeeded")
  368. let temp = TempBasal(duration: Int(duration / 60), rate: Decimal(rate), temp: .absolute, timestamp: Date())
  369. self.storage.save(temp, as: OpenAPS.Monitor.tempBasal)
  370. if rate == 0, duration == 0 {
  371. self.pumpHistoryStorage.saveCancelTempEvents()
  372. }
  373. }
  374. }
  375. }
  376. func dailyAutotune() -> AnyPublisher<Bool, Never> {
  377. guard settings.useAutotune else {
  378. return Just(false).eraseToAnyPublisher()
  379. }
  380. let now = Date()
  381. guard lastAutotuneDate.isBeforeDate(now, granularity: .day) else {
  382. return Just(false).eraseToAnyPublisher()
  383. }
  384. lastAutotuneDate = now
  385. return autotune().map { $0 != nil }.eraseToAnyPublisher()
  386. }
  387. func autotune() -> AnyPublisher<Autotune?, Never> {
  388. openAPS.autotune().eraseToAnyPublisher()
  389. }
  390. func enactAnnouncement(_ announcement: Announcement) {
  391. guard let action = announcement.action else {
  392. warning(.apsManager, "Invalid Announcement action")
  393. return
  394. }
  395. guard let pump = pumpManager else {
  396. warning(.apsManager, "Pump is not set")
  397. return
  398. }
  399. debug(.apsManager, "Start enact announcement: \(action)")
  400. switch action {
  401. case let .bolus(amount):
  402. if let error = verifyStatus() {
  403. processError(error)
  404. return
  405. }
  406. let roundedAmount = pump.roundToSupportedBolusVolume(units: Double(amount))
  407. pump.enactBolus(units: roundedAmount, activationType: .manualRecommendationAccepted) { error in
  408. if let error = error {
  409. // warning(.apsManager, "Announcement Bolus failed with error: \(error.localizedDescription)")
  410. switch error {
  411. case .uncertainDelivery:
  412. // Do not generate notification on uncertain delivery error
  413. break
  414. default:
  415. // Do not generate notifications for automatic boluses that fail.
  416. warning(.apsManager, "Announcement Bolus failed with error: \(error.localizedDescription)")
  417. }
  418. } else {
  419. debug(.apsManager, "Announcement Bolus succeeded")
  420. self.announcementsStorage.storeAnnouncements([announcement], enacted: true)
  421. self.bolusProgress.send(0)
  422. }
  423. }
  424. case let .pump(pumpAction):
  425. switch pumpAction {
  426. case .suspend:
  427. if let error = verifyStatus() {
  428. processError(error)
  429. return
  430. }
  431. pump.suspendDelivery { error in
  432. if let error = error {
  433. debug(.apsManager, "Pump not suspended by Announcement: \(error.localizedDescription)")
  434. } else {
  435. debug(.apsManager, "Pump suspended by Announcement")
  436. self.announcementsStorage.storeAnnouncements([announcement], enacted: true)
  437. self.nightscout.uploadStatus()
  438. }
  439. }
  440. case .resume:
  441. guard pump.status.pumpStatus.suspended else {
  442. return
  443. }
  444. pump.resumeDelivery { error in
  445. if let error = error {
  446. warning(.apsManager, "Pump not resumed by Announcement: \(error.localizedDescription)")
  447. } else {
  448. debug(.apsManager, "Pump resumed by Announcement")
  449. self.announcementsStorage.storeAnnouncements([announcement], enacted: true)
  450. self.nightscout.uploadStatus()
  451. }
  452. }
  453. }
  454. case let .looping(closedLoop):
  455. settings.closedLoop = closedLoop
  456. debug(.apsManager, "Closed loop \(closedLoop) by Announcement")
  457. announcementsStorage.storeAnnouncements([announcement], enacted: true)
  458. case let .tempbasal(rate, duration):
  459. if let error = verifyStatus() {
  460. processError(error)
  461. return
  462. }
  463. // unable to do temp basal during manual temp basal 😁
  464. if isManualTempBasal {
  465. processError(APSError.manualBasalTemp(message: "Loop not possible during the manual basal temp"))
  466. return
  467. }
  468. guard !settings.closedLoop else {
  469. return
  470. }
  471. let roundedRate = pump.roundToSupportedBasalRate(unitsPerHour: Double(rate))
  472. pump.enactTempBasal(unitsPerHour: roundedRate, for: TimeInterval(duration) * 60) { error in
  473. if let error = error {
  474. warning(.apsManager, "Announcement TempBasal failed with error: \(error.localizedDescription)")
  475. } else {
  476. debug(.apsManager, "Announcement TempBasal succeeded")
  477. self.announcementsStorage.storeAnnouncements([announcement], enacted: true)
  478. }
  479. }
  480. }
  481. }
  482. private func currentTemp(date: Date) -> TempBasal {
  483. let defaultTemp = { () -> TempBasal in
  484. guard let temp = storage.retrieve(OpenAPS.Monitor.tempBasal, as: TempBasal.self) else {
  485. return TempBasal(duration: 0, rate: 0, temp: .absolute, timestamp: Date())
  486. }
  487. let delta = Int((date.timeIntervalSince1970 - temp.timestamp.timeIntervalSince1970) / 60)
  488. let duration = max(0, temp.duration - delta)
  489. return TempBasal(duration: duration, rate: temp.rate, temp: .absolute, timestamp: date)
  490. }()
  491. guard let state = pumpManager?.status.basalDeliveryState else { return defaultTemp }
  492. switch state {
  493. case .active:
  494. return TempBasal(duration: 0, rate: 0, temp: .absolute, timestamp: date)
  495. case let .tempBasal(dose):
  496. let rate = Decimal(dose.unitsPerHour)
  497. let durationMin = max(0, Int((dose.endDate.timeIntervalSince1970 - date.timeIntervalSince1970) / 60))
  498. return TempBasal(duration: durationMin, rate: rate, temp: .absolute, timestamp: date)
  499. default:
  500. return defaultTemp
  501. }
  502. }
  503. private func enactSuggested() -> AnyPublisher<Void, Error> {
  504. guard let suggested = storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self) else {
  505. return Fail(error: APSError.apsError(message: "Suggestion not found")).eraseToAnyPublisher()
  506. }
  507. guard Date().timeIntervalSince(suggested.deliverAt ?? .distantPast) < Config.eхpirationInterval else {
  508. return Fail(error: APSError.apsError(message: "Suggestion expired")).eraseToAnyPublisher()
  509. }
  510. guard let pump = pumpManager else {
  511. return Fail(error: APSError.apsError(message: "Pump not set")).eraseToAnyPublisher()
  512. }
  513. // unable to do temp basal during manual temp basal 😁
  514. if isManualTempBasal {
  515. return Fail(error: APSError.manualBasalTemp(message: "Loop not possible during the manual basal temp"))
  516. .eraseToAnyPublisher()
  517. }
  518. let basalPublisher: AnyPublisher<Void, Error> = Deferred { () -> AnyPublisher<Void, Error> in
  519. if let error = self.verifyStatus() {
  520. return Fail(error: error).eraseToAnyPublisher()
  521. }
  522. guard let rate = suggested.rate, let duration = suggested.duration else {
  523. // It is OK, no temp required
  524. debug(.apsManager, "No temp required")
  525. return Just(()).setFailureType(to: Error.self)
  526. .eraseToAnyPublisher()
  527. }
  528. return pump.enactTempBasal(unitsPerHour: Double(rate), for: TimeInterval(duration * 60)).map { _ in
  529. let temp = TempBasal(duration: duration, rate: rate, temp: .absolute, timestamp: Date())
  530. self.storage.save(temp, as: OpenAPS.Monitor.tempBasal)
  531. return ()
  532. }
  533. .eraseToAnyPublisher()
  534. }.eraseToAnyPublisher()
  535. let bolusPublisher: AnyPublisher<Void, Error> = Deferred { () -> AnyPublisher<Void, Error> in
  536. if let error = self.verifyStatus() {
  537. return Fail(error: error).eraseToAnyPublisher()
  538. }
  539. guard let units = suggested.units else {
  540. // It is OK, no bolus required
  541. debug(.apsManager, "No bolus required")
  542. return Just(()).setFailureType(to: Error.self)
  543. .eraseToAnyPublisher()
  544. }
  545. return pump.enactBolus(units: Double(units), automatic: true).map { _ in
  546. self.bolusProgress.send(0)
  547. return ()
  548. }
  549. .eraseToAnyPublisher()
  550. }.eraseToAnyPublisher()
  551. return basalPublisher.flatMap { bolusPublisher }.eraseToAnyPublisher()
  552. }
  553. private func reportEnacted(received: Bool) {
  554. if let suggestion = storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self), suggestion.deliverAt != nil {
  555. var enacted = suggestion
  556. enacted.timestamp = Date()
  557. enacted.recieved = received
  558. storage.save(enacted, as: OpenAPS.Enact.enacted)
  559. // Create a tdd.json
  560. tdd(enacted_: enacted)
  561. // Create a dailyStats.json
  562. dailyStats()
  563. debug(.apsManager, "Suggestion enacted. Received: \(received)")
  564. DispatchQueue.main.async {
  565. self.broadcaster.notify(EnactedSuggestionObserver.self, on: .main) {
  566. $0.enactedSuggestionDidUpdate(enacted)
  567. }
  568. }
  569. nightscout.uploadStatus()
  570. }
  571. }
  572. func tdd(enacted_: Suggestion) {
  573. // Add to tdd.json:
  574. let preferences = settingsManager.preferences
  575. let currentTDD = enacted_.tdd ?? 0
  576. let file = OpenAPS.Monitor.tdd
  577. let tdd = TDD(
  578. TDD: currentTDD,
  579. timestamp: Date(),
  580. id: UUID().uuidString
  581. )
  582. var uniqEvents: [TDD] = []
  583. storage.transaction { storage in
  584. storage.append(tdd, to: file, uniqBy: \.id)
  585. uniqEvents = storage.retrieve(file, as: [TDD].self)?
  586. .filter { $0.timestamp.addingTimeInterval(14.days.timeInterval) > Date() }
  587. .sorted { $0.timestamp > $1.timestamp } ?? []
  588. var total: Decimal = 0
  589. var indeces: Decimal = 0
  590. for uniqEvent in uniqEvents {
  591. if uniqEvent.TDD > 0 {
  592. total += uniqEvent.TDD
  593. indeces += 1
  594. }
  595. }
  596. let entriesPast2hours = storage.retrieve(file, as: [TDD].self)?
  597. .filter { $0.timestamp.addingTimeInterval(2.hours.timeInterval) > Date() }
  598. .sorted { $0.timestamp > $1.timestamp } ?? []
  599. var totalAmount: Decimal = 0
  600. var nrOfIndeces: Decimal = 0
  601. for entry in entriesPast2hours {
  602. if entry.TDD > 0 {
  603. totalAmount += entry.TDD
  604. nrOfIndeces += 1
  605. }
  606. }
  607. if indeces == 0 {
  608. indeces = 1
  609. }
  610. if nrOfIndeces == 0 {
  611. nrOfIndeces = 1
  612. }
  613. let average14 = total / indeces
  614. let average2hours = totalAmount / nrOfIndeces
  615. let weight = preferences.weightPercentage
  616. let weighted_average = weight * average2hours + (1 - weight) * average14
  617. let averages = TDD_averages(
  618. average_total_data: roundDecimal(average14, 1),
  619. weightedAverage: roundDecimal(weighted_average, 1),
  620. past2hoursAverage: roundDecimal(average2hours, 1),
  621. date: Date()
  622. )
  623. storage.save(averages, as: OpenAPS.Monitor.tdd_averages)
  624. storage.save(Array(uniqEvents), as: file)
  625. }
  626. }
  627. private func roundDecimal(_ decimal: Decimal, _ digits: Double) -> Decimal {
  628. let rounded = round(Double(decimal) * pow(10, digits)) / pow(10, digits)
  629. return Decimal(rounded)
  630. }
  631. func dailyStats() {
  632. // Add to dailyStats.JSON
  633. let preferences = settingsManager.preferences
  634. let carbs = storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self)
  635. let tdds = storage.retrieve(OpenAPS.Monitor.tdd, as: [TDD].self)
  636. let currentTDD = tdds?[0].TDD
  637. let carbs_length = carbs?.count ?? 0
  638. var carbTotal: Decimal = 0
  639. if carbs_length != 0 {
  640. for each in carbs! {
  641. if each.carbs != 0 {
  642. carbTotal += each.carbs
  643. }
  644. }
  645. }
  646. var algo_ = "oref0"
  647. if preferences.enableChris, preferences.useNewFormula {
  648. algo_ = "Dynamic ISF, Logarithmic Formula"
  649. } else if !preferences.useNewFormula, preferences.enableChris {
  650. algo_ = "Dynamic ISF, Original Formula"
  651. }
  652. let af = preferences.adjustmentFactor
  653. let insulin_type = preferences.curve
  654. var buildDate: Date {
  655. if let infoPath = Bundle.main.path(forResource: "Info", ofType: "plist"),
  656. let infoAttr = try? FileManager.default.attributesOfItem(atPath: infoPath),
  657. let infoDate = infoAttr[.modificationDate] as? Date
  658. {
  659. return infoDate
  660. }
  661. return Date()
  662. }
  663. let nsObject: AnyObject? = Bundle.main.infoDictionary!["CFBundleShortVersionString"] as AnyObject
  664. let version = nsObject as! String
  665. let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String
  666. let branch = Bundle.main.infoDictionary?["NSHumanReadableCopyright"] as? String
  667. let pump_ = pumpManager?.localizedTitle ?? ""
  668. let cgm = settingsManager.settings.cgm
  669. var file = OpenAPS.Monitor.dailyStats
  670. var iPa: Decimal = 75
  671. if preferences.useCustomPeakTime {
  672. iPa = preferences.insulinPeakTime
  673. } else if preferences.curve.rawValue == "rapid-acting" {
  674. iPa = 65
  675. } else if preferences.curve.rawValue == "ultra-rapid" {
  676. iPa = 50
  677. }
  678. // HbA1c estimation (%, mmol/mol)
  679. let NGSPa1CStatisticValue = (46.7 + tir().averageGlucose_1) / 28.7 // NGSP (%)
  680. let IFCCa1CStatisticValue = 10.929 *
  681. (NGSPa1CStatisticValue - 2.152) // IFCC (mmol/mol) A1C(mmol/mol) = 10.929 * (A1C(%) - 2.15)
  682. // 7 days
  683. let NGSPa1CStatisticValue_7 = (46.7 + tir().averageGlucose_7) / 28.7
  684. let IFCCa1CStatisticValue_7 = 10.929 * (NGSPa1CStatisticValue_7 - 2.152)
  685. // 14 days
  686. let NGSPa1CStatisticValue_30 = (46.7 + tir().averageGlucose_30) / 28.7
  687. let IFCCa1CStatisticValue_30 = 10.929 * (NGSPa1CStatisticValue_30 - 2.152)
  688. // All days
  689. let NGSPa1CStatisticValue_total = (46.7 + tir().averageGlucose) / 28.7
  690. let IFCCa1CStatisticValue_total = 10.929 * (NGSPa1CStatisticValue_total - 2.152)
  691. // HbA1c string and BG string:
  692. var HbA1c_string_1 = ""
  693. var string7Days = ""
  694. var string30Days = ""
  695. var stringTotal = ""
  696. var bgString1day = ""
  697. var bgString7Days = ""
  698. var bgString30Days = ""
  699. var bgAverageTotalString = ""
  700. let daysBG = tir().daysWithBG
  701. print("Days with BG: \(daysBG)")
  702. let avg1 = tir().averageGlucose_1
  703. let avg7 = tir().averageGlucose_7
  704. let avg30 = tir().averageGlucose_30
  705. let avgTot = tir().averageGlucose
  706. if avg1 != 0 {
  707. bgString1day = " Average BG (mmol/l, 1 day): \(roundDecimal(avg1 * 0.0555, 1)). Average BG (mmg/dl, 1 day): \(avg1)."
  708. HbA1c_string_1 =
  709. "Estimated HbA1c (%, 1 day): \(roundDecimal(NGSPa1CStatisticValue, 1)). Estimated HbA1c (mmol/mol, 1 day): \(roundDecimal(IFCCa1CStatisticValue, 1))."
  710. }
  711. if avg7 != 0 {
  712. string7Days =
  713. " HbA1c 7 days (mmol/mol): \(roundDecimal(IFCCa1CStatisticValue_7, 1)). HbA1c 7 days (%): \(roundDecimal(NGSPa1CStatisticValue_7, 1))."
  714. bgString7Days = " Average BG (mmol/l) 7 days: \(roundDecimal(avg7 * 0.0555, 1)). Average BG (mg/dl) 7 days: \(avg7)."
  715. }
  716. if avg30 != 0 {
  717. string30Days =
  718. " HbA1c 30 days (mmol/mol): \(roundDecimal(IFCCa1CStatisticValue_30, 1)). HbA1c 30 days (%): \(roundDecimal(NGSPa1CStatisticValue_30, 1))."
  719. bgString30Days =
  720. " Average BG 30 days (mmol/l): \(roundDecimal(avg30 * 0.0555, 1)). Average BG 30 days (mg/dl): \(avg30). "
  721. }
  722. if avgTot != 0, daysBG >= 2 {
  723. stringTotal =
  724. " HbA1c Total (\(daysBG)) Days (mmol/mol): \(roundDecimal(IFCCa1CStatisticValue_total, 1)). HbA1c Total (\(daysBG)) Days (mg/dl): \(roundDecimal(NGSPa1CStatisticValue_total, 1)) %."
  725. bgAverageTotalString =
  726. "BG Average Total (\(daysBG)) Days (mmol/l): \(roundDecimal(avgTot * 0.0555, 1)). BG Average Total (\(daysBG)) Days (mmg/dl): \(avgTot)."
  727. }
  728. let HbA1c_string = HbA1c_string_1 + string7Days + string30Days + stringTotal
  729. var tirString =
  730. "TIR (24 hours): \(tir().TIR_1) %. Time with Hypoglucemia: \(tir().hypos_1) % (< 4 / 72). Time with Hyperglucemia: \(tir().hypers_1) % (> 10 / 180)."
  731. if daysBG >= 2 {
  732. tirString +=
  733. " Total days (\(daysBG)) TIR: \(tir().TIR) %. Time with Hypoglucemia: \(tir().hypos) % (< 4 / 72). Time with Hyperglucemia: \(tir().hypers) % (> 10 / 180)."
  734. }
  735. let bgAverageString = bgString1day + bgString7Days + bgString30Days + bgAverageTotalString
  736. let dailystat = DailyStats(
  737. createdAt: Date(),
  738. FAX_Build_Version: version,
  739. FAX_Build_Number: build ?? "1",
  740. FAX_Branch: branch ?? "N/A",
  741. FAX_Build_Date: buildDate,
  742. Algorithm: algo_,
  743. AdjustmentFactor: af,
  744. Pump: pump_,
  745. CGM: cgm.rawValue,
  746. insulinType: insulin_type.rawValue,
  747. peakActivityTime: iPa,
  748. TDD: currentTDD ?? 0,
  749. Carbs_24h: carbTotal,
  750. TIR: tirString,
  751. BG_Average: bgAverageString,
  752. HbA1c: HbA1c_string
  753. )
  754. file = OpenAPS.Monitor.dailyStats
  755. storage.save(dailystat, as: file)
  756. }
  757. // Time In Range (%) and Average Glucose (24 hours). This function looks dumb. I will refactor it later.
  758. func tir()
  759. -> (
  760. averageGlucose: Decimal,
  761. averageGlucose_1: Decimal,
  762. averageGlucose_7: Decimal,
  763. averageGlucose_30: Decimal,
  764. hypos: Decimal,
  765. hypers: Decimal,
  766. TIR: Decimal,
  767. hypos_1: Decimal,
  768. hypers_1: Decimal,
  769. TIR_1: Decimal,
  770. daysWithBG: Decimal
  771. )
  772. {
  773. let glucose = storage.retrieve(OpenAPS.Monitor.glucose, as: [BloodGlucose].self)
  774. let length_ = glucose?.count ?? 0
  775. let endIndex = length_ - 1
  776. var oneDayGlucoseIndex = endIndex
  777. guard length_ != 0 else {
  778. return (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
  779. }
  780. var bg: Decimal = 0
  781. var nr_bgs: Decimal = 0
  782. let startDate = glucose![0].date
  783. var end1 = false
  784. var end7 = false
  785. var end30 = false
  786. var bg_1: Decimal = 0
  787. var bg_7: Decimal = 0
  788. var bg_30: Decimal = 0
  789. var j = -1
  790. for entry in glucose! {
  791. j += 1
  792. if entry.glucose! > 0 {
  793. bg += Decimal(entry.glucose!)
  794. nr_bgs += 1
  795. if startDate - entry.date >= 8.64E7, !end1 {
  796. end1 = true
  797. oneDayGlucoseIndex = j
  798. bg_1 = bg / nr_bgs
  799. }
  800. if startDate - entry.date >= 6.045E8, !end7 {
  801. end7 = true
  802. bg_7 = bg / nr_bgs
  803. }
  804. if startDate - entry.date > 2.59E9, !end30 {
  805. end30 = true
  806. bg_30 = bg / nr_bgs
  807. }
  808. }
  809. }
  810. let bg_90 = bg / nr_bgs
  811. let fullTime = glucose![0].date - glucose![endIndex].date
  812. var fullTime_1 = glucose![0].date - glucose![oneDayGlucoseIndex].date
  813. let daysBG = fullTime / 8.64E7
  814. var timeInHypo: Decimal = 0
  815. var timeInHyper: Decimal = 0
  816. var hypos: Decimal = 0
  817. var hypers: Decimal = 0
  818. var i = -1
  819. var lastIndex = false
  820. while i < endIndex {
  821. i += 1
  822. let currentTime = glucose![i].date
  823. var previousTime = currentTime
  824. if i + 1 <= endIndex {
  825. previousTime = glucose![i + 1].date
  826. } else {
  827. lastIndex = true
  828. }
  829. if glucose![i].glucose! < 72, !lastIndex {
  830. timeInHypo += currentTime - previousTime
  831. } else if glucose![i].glucose! > 180, !lastIndex {
  832. timeInHyper += currentTime - previousTime
  833. }
  834. }
  835. if timeInHypo == 0 {
  836. hypos = 0
  837. } else { hypos = (timeInHypo / fullTime) * 100
  838. }
  839. if timeInHyper == 0 {
  840. hypers = 0
  841. } else { hypers = (timeInHyper / fullTime) * 100
  842. }
  843. let TIR = 100 - (hypos + hypers)
  844. // Do the loop again but with for 1 day. I will change this later, because this looks really dumb:
  845. var timeInHypo_1: Decimal = 0
  846. var timeInHyper_1: Decimal = 0
  847. var hypos_1: Decimal = 0
  848. var hypers_1: Decimal = 0
  849. i = -1
  850. lastIndex = false
  851. while i < oneDayGlucoseIndex {
  852. i += 1
  853. let currentTime = glucose![i].date
  854. var previousTime = currentTime
  855. if i + 1 <= oneDayGlucoseIndex {
  856. previousTime = glucose![i + 1].date
  857. } else {
  858. lastIndex = true
  859. }
  860. if glucose![i].glucose! < 72, !lastIndex {
  861. timeInHypo_1 += currentTime - previousTime
  862. } else if glucose![i].glucose! > 180, !lastIndex {
  863. timeInHyper_1 += currentTime - previousTime
  864. }
  865. }
  866. if timeInHypo_1 == 0 {
  867. hypos_1 = 0
  868. } else { hypos_1 = (timeInHypo_1 / fullTime_1) * 100
  869. }
  870. if timeInHyper_1 == 0 {
  871. hypers_1 = 0
  872. } else { hypers_1 = (timeInHyper_1 / fullTime_1) * 100
  873. }
  874. let TIR_1 = 100 - (hypos_1 + hypers_1)
  875. return (
  876. roundDecimal(bg_90, 0),
  877. roundDecimal(bg_1, 0),
  878. roundDecimal(bg_7, 0),
  879. roundDecimal(bg_30, 0),
  880. roundDecimal(hypos, 1),
  881. roundDecimal(hypers, 1),
  882. roundDecimal(TIR, 1),
  883. roundDecimal(hypos_1, 1),
  884. roundDecimal(hypers_1, 1),
  885. roundDecimal(TIR_1, 1),
  886. roundDecimal(daysBG, 1)
  887. )
  888. }
  889. private func loadFileFromStorage(name: String) -> RawJSON {
  890. storage.retrieveRaw(name) ?? OpenAPS.defaults(for: name)
  891. }
  892. private func processError(_ error: Error) {
  893. warning(.apsManager, "\(error.localizedDescription)")
  894. lastError.send(error)
  895. }
  896. private func createBolusReporter() {
  897. bolusReporter = pumpManager?.createBolusProgressReporter(reportingOn: processQueue)
  898. bolusReporter?.addObserver(self)
  899. }
  900. private func updateStatus() {
  901. debug(.apsManager, "force update status")
  902. guard let pump = pumpManager else {
  903. return
  904. }
  905. if let omnipod = pump as? OmnipodPumpManager {
  906. omnipod.getPodStatus { _ in }
  907. }
  908. if let omnipodBLE = pump as? OmniBLEPumpManager {
  909. omnipodBLE.getPodStatus { _ in }
  910. }
  911. }
  912. private func clearBolusReporter() {
  913. bolusReporter?.removeObserver(self)
  914. bolusReporter = nil
  915. processQueue.asyncAfter(deadline: .now() + 0.5) {
  916. self.bolusProgress.send(nil)
  917. self.updateStatus()
  918. }
  919. }
  920. }
  921. private extension PumpManager {
  922. func enactTempBasal(unitsPerHour: Double, for duration: TimeInterval) -> AnyPublisher<DoseEntry?, Error> {
  923. Future { promise in
  924. self.enactTempBasal(unitsPerHour: unitsPerHour, for: duration) { error in
  925. if let error = error {
  926. debug(.apsManager, "Temp basal failed: \(unitsPerHour) for: \(duration)")
  927. promise(.failure(error))
  928. } else {
  929. debug(.apsManager, "Temp basal succeded: \(unitsPerHour) for: \(duration)")
  930. promise(.success(nil))
  931. }
  932. }
  933. }
  934. .mapError { APSError.pumpError($0) }
  935. .eraseToAnyPublisher()
  936. }
  937. func enactBolus(units: Double, automatic: Bool) -> AnyPublisher<DoseEntry?, Error> {
  938. Future { promise in
  939. // convert automatic
  940. let automaticValue = automatic ? BolusActivationType.automatic : BolusActivationType.manualRecommendationAccepted
  941. self.enactBolus(units: units, activationType: automaticValue) { error in
  942. if let error = error {
  943. debug(.apsManager, "Bolus failed: \(units)")
  944. promise(.failure(error))
  945. } else {
  946. debug(.apsManager, "Bolus succeded: \(units)")
  947. promise(.success(nil))
  948. }
  949. }
  950. }
  951. .mapError { APSError.pumpError($0) }
  952. .eraseToAnyPublisher()
  953. }
  954. func cancelBolus() -> AnyPublisher<DoseEntry?, Error> {
  955. Future { promise in
  956. self.cancelBolus { result in
  957. switch result {
  958. case let .success(dose):
  959. debug(.apsManager, "Cancel Bolus succeded")
  960. promise(.success(dose))
  961. case let .failure(error):
  962. debug(.apsManager, "Cancel Bolus failed")
  963. promise(.failure(error))
  964. }
  965. }
  966. }
  967. .mapError { APSError.pumpError($0) }
  968. .eraseToAnyPublisher()
  969. }
  970. func suspendDelivery() -> AnyPublisher<Void, Error> {
  971. Future { promise in
  972. self.suspendDelivery { error in
  973. if let error = error {
  974. promise(.failure(error))
  975. } else {
  976. promise(.success(()))
  977. }
  978. }
  979. }
  980. .mapError { APSError.pumpError($0) }
  981. .eraseToAnyPublisher()
  982. }
  983. func resumeDelivery() -> AnyPublisher<Void, Error> {
  984. Future { promise in
  985. self.resumeDelivery { error in
  986. if let error = error {
  987. promise(.failure(error))
  988. } else {
  989. promise(.success(()))
  990. }
  991. }
  992. }
  993. .mapError { APSError.pumpError($0) }
  994. .eraseToAnyPublisher()
  995. }
  996. }
  997. extension BaseAPSManager: PumpManagerStatusObserver {
  998. func pumpManager(_: PumpManager, didUpdate status: PumpManagerStatus, oldStatus _: PumpManagerStatus) {
  999. let percent = Int((status.pumpBatteryChargeRemaining ?? 1) * 100)
  1000. let battery = Battery(
  1001. percent: percent,
  1002. voltage: nil,
  1003. string: percent > 10 ? .normal : .low,
  1004. display: status.pumpBatteryChargeRemaining != nil
  1005. )
  1006. storage.save(battery, as: OpenAPS.Monitor.battery)
  1007. storage.save(status.pumpStatus, as: OpenAPS.Monitor.status)
  1008. }
  1009. }
  1010. extension BaseAPSManager: DoseProgressObserver {
  1011. func doseProgressReporterDidUpdate(_ doseProgressReporter: DoseProgressReporter) {
  1012. bolusProgress.send(Decimal(doseProgressReporter.progress.percentComplete))
  1013. if doseProgressReporter.progress.isComplete {
  1014. clearBolusReporter()
  1015. }
  1016. }
  1017. }
  1018. extension PumpManagerStatus {
  1019. var pumpStatus: PumpStatus {
  1020. let bolusing = bolusState != .noBolus
  1021. let suspended = basalDeliveryState?.isSuspended ?? true
  1022. let type = suspended ? StatusType.suspended : (bolusing ? .bolusing : .normal)
  1023. return PumpStatus(status: type, bolusing: bolusing, suspended: suspended, timestamp: Date())
  1024. }
  1025. }