APSManager.swift 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865
  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 glucoseStorage: GlucoseStorage!
  62. @Injected() private var tempTargetsStorage: TempTargetsStorage!
  63. @Injected() private var carbsStorage: CarbsStorage!
  64. @Injected() private var announcementsStorage: AnnouncementsStorage!
  65. @Injected() private var deviceDataManager: DeviceDataManager!
  66. @Injected() private var nightscout: NightscoutManager!
  67. @Injected() private var settingsManager: SettingsManager!
  68. @Injected() private var broadcaster: Broadcaster!
  69. @Persisted(key: "lastAutotuneDate") private var lastAutotuneDate = Date()
  70. @Persisted(key: "lastLoopDate") var lastLoopDate: Date = .distantPast {
  71. didSet {
  72. lastLoopDateSubject.send(lastLoopDate)
  73. }
  74. }
  75. private var openAPS: OpenAPS!
  76. private var lifetime = Lifetime()
  77. var pumpManager: PumpManagerUI? {
  78. get { deviceDataManager.pumpManager }
  79. set { deviceDataManager.pumpManager = newValue }
  80. }
  81. var bluetoothManager: BluetoothStateManager? { deviceDataManager.bluetoothManager }
  82. @Persisted(key: "isManualTempBasal") var isManualTempBasal: Bool = false
  83. let isLooping = CurrentValueSubject<Bool, Never>(false)
  84. let lastLoopDateSubject = PassthroughSubject<Date, Never>()
  85. let lastError = CurrentValueSubject<Error?, Never>(nil)
  86. let bolusProgress = CurrentValueSubject<Decimal?, Never>(nil)
  87. var pumpDisplayState: CurrentValueSubject<PumpDisplayState?, Never> {
  88. deviceDataManager.pumpDisplayState
  89. }
  90. var pumpName: CurrentValueSubject<String, Never> {
  91. deviceDataManager.pumpName
  92. }
  93. var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> {
  94. deviceDataManager.pumpExpiresAtDate
  95. }
  96. var settings: FreeAPSSettings {
  97. get { settingsManager.settings }
  98. set { settingsManager.settings = newValue }
  99. }
  100. init(resolver: Resolver) {
  101. injectServices(resolver)
  102. openAPS = OpenAPS(storage: storage)
  103. subscribe()
  104. lastLoopDateSubject.send(lastLoopDate)
  105. isLooping
  106. .weakAssign(to: \.deviceDataManager.loopInProgress, on: self)
  107. .store(in: &lifetime)
  108. }
  109. private func subscribe() {
  110. deviceDataManager.recommendsLoop
  111. .receive(on: processQueue)
  112. .sink { [weak self] in
  113. self?.loop()
  114. }
  115. .store(in: &lifetime)
  116. pumpManager?.addStatusObserver(self, queue: processQueue)
  117. deviceDataManager.errorSubject
  118. .receive(on: processQueue)
  119. .map { APSError.pumpError($0) }
  120. .sink {
  121. self.processError($0)
  122. }
  123. .store(in: &lifetime)
  124. deviceDataManager.bolusTrigger
  125. .receive(on: processQueue)
  126. .sink { bolusing in
  127. if bolusing {
  128. self.createBolusReporter()
  129. } else {
  130. self.clearBolusReporter()
  131. }
  132. }
  133. .store(in: &lifetime)
  134. // manage a manual Temp Basal from OmniPod - Force loop() after stop a temp basal or finished
  135. deviceDataManager.manualTempBasal
  136. .receive(on: processQueue)
  137. .sink { manualBasal in
  138. if manualBasal {
  139. self.isManualTempBasal = true
  140. } else {
  141. if self.isManualTempBasal {
  142. self.isManualTempBasal = false
  143. self.loop()
  144. }
  145. }
  146. }
  147. .store(in: &lifetime)
  148. }
  149. func heartbeat(date: Date) {
  150. deviceDataManager.heartbeat(date: date)
  151. }
  152. // Loop entry point
  153. private func loop() {
  154. guard !isLooping.value else {
  155. warning(.apsManager, "Already looping, skip")
  156. return
  157. }
  158. debug(.apsManager, "Starting loop")
  159. isLooping.send(true)
  160. determineBasal()
  161. .replaceEmpty(with: false)
  162. .flatMap { [weak self] success -> AnyPublisher<Void, Error> in
  163. guard let self = self, success else {
  164. return Fail(error: APSError.apsError(message: "Determine basal failed")).eraseToAnyPublisher()
  165. }
  166. // Open loop completed
  167. guard self.settings.closedLoop else {
  168. return Just(()).setFailureType(to: Error.self).eraseToAnyPublisher()
  169. }
  170. self.nightscout.uploadStatus()
  171. // Closed loop - enact suggested
  172. return self.enactSuggested()
  173. }
  174. .sink { [weak self] completion in
  175. guard let self = self else { return }
  176. if case let .failure(error) = completion {
  177. self.loopCompleted(error: error)
  178. } else {
  179. self.loopCompleted()
  180. }
  181. } receiveValue: {}
  182. .store(in: &lifetime)
  183. }
  184. // Loop exit point
  185. private func loopCompleted(error: Error? = nil) {
  186. isLooping.send(false)
  187. if let error = error {
  188. warning(.apsManager, "Loop failed with error: \(error.localizedDescription)")
  189. processError(error)
  190. } else {
  191. debug(.apsManager, "Loop succeeded")
  192. lastLoopDate = Date()
  193. lastError.send(nil)
  194. }
  195. if settings.closedLoop {
  196. reportEnacted(received: error == nil)
  197. }
  198. }
  199. private func verifyStatus() -> Error? {
  200. guard let pump = pumpManager else {
  201. return APSError.invalidPumpState(message: "Pump not set")
  202. }
  203. let status = pump.status.pumpStatus
  204. guard !status.bolusing else {
  205. return APSError.invalidPumpState(message: "Pump is bolusing")
  206. }
  207. guard !status.suspended else {
  208. return APSError.invalidPumpState(message: "Pump suspended")
  209. }
  210. let reservoir = storage.retrieve(OpenAPS.Monitor.reservoir, as: Decimal.self) ?? 100
  211. guard reservoir >= 0 else {
  212. return APSError.invalidPumpState(message: "Reservoir is empty")
  213. }
  214. return nil
  215. }
  216. private func autosens() -> AnyPublisher<Bool, Never> {
  217. guard let autosens = storage.retrieve(OpenAPS.Settings.autosense, as: Autosens.self),
  218. (autosens.timestamp ?? .distantPast).addingTimeInterval(30.minutes.timeInterval) > Date()
  219. else {
  220. return openAPS.autosense()
  221. .map { $0 != nil }
  222. .eraseToAnyPublisher()
  223. }
  224. return Just(false).eraseToAnyPublisher()
  225. }
  226. func determineBasal() -> AnyPublisher<Bool, Never> {
  227. debug(.apsManager, "Start determine basal")
  228. guard let glucose = storage.retrieve(OpenAPS.Monitor.glucose, as: [BloodGlucose].self), glucose.isNotEmpty else {
  229. debug(.apsManager, "Not enough glucose data")
  230. processError(APSError.glucoseError(message: "Not enough glucose data"))
  231. return Just(false).eraseToAnyPublisher()
  232. }
  233. let lastGlucoseDate = glucoseStorage.lastGlucoseDate()
  234. guard lastGlucoseDate >= Date().addingTimeInterval(-12.minutes.timeInterval) else {
  235. debug(.apsManager, "Glucose data is stale")
  236. processError(APSError.glucoseError(message: "Glucose data is stale"))
  237. return Just(false).eraseToAnyPublisher()
  238. }
  239. guard glucoseStorage.isGlucoseNotFlat() else {
  240. debug(.apsManager, "Glucose data is too flat")
  241. processError(APSError.glucoseError(message: "Glucose data is too flat"))
  242. return Just(false).eraseToAnyPublisher()
  243. }
  244. let now = Date()
  245. let temp = currentTemp(date: now)
  246. let mainPublisher = makeProfiles()
  247. .flatMap { _ in self.autosens() }
  248. .flatMap { _ in self.dailyAutotune() }
  249. .flatMap { _ in self.openAPS.determineBasal(currentTemp: temp, clock: now) }
  250. .map { suggestion -> Bool in
  251. if let suggestion = suggestion {
  252. DispatchQueue.main.async {
  253. self.broadcaster.notify(SuggestionObserver.self, on: .main) {
  254. $0.suggestionDidUpdate(suggestion)
  255. }
  256. }
  257. }
  258. return suggestion != nil
  259. }
  260. .eraseToAnyPublisher()
  261. if temp.duration == 0,
  262. settings.closedLoop,
  263. settingsManager.preferences.unsuspendIfNoTemp,
  264. let pump = pumpManager,
  265. pump.status.pumpStatus.suspended
  266. {
  267. return pump.resumeDelivery()
  268. .flatMap { _ in mainPublisher }
  269. .replaceError(with: false)
  270. .eraseToAnyPublisher()
  271. }
  272. return mainPublisher
  273. }
  274. func determineBasalSync() {
  275. determineBasal().cancellable().store(in: &lifetime)
  276. }
  277. func makeProfiles() -> AnyPublisher<Bool, Never> {
  278. openAPS.makeProfiles(useAutotune: settings.useAutotune)
  279. .map { tunedProfile in
  280. if let basalProfile = tunedProfile?.basalProfile {
  281. self.processQueue.async {
  282. self.broadcaster.notify(BasalProfileObserver.self, on: self.processQueue) {
  283. $0.basalProfileDidChange(basalProfile)
  284. }
  285. }
  286. }
  287. return tunedProfile != nil
  288. }
  289. .eraseToAnyPublisher()
  290. }
  291. func roundBolus(amount: Decimal) -> Decimal {
  292. guard let pump = pumpManager else { return amount }
  293. let rounded = Decimal(pump.roundToSupportedBolusVolume(units: Double(amount)))
  294. let maxBolus = Decimal(pump.roundToSupportedBolusVolume(units: Double(settingsManager.pumpSettings.maxBolus)))
  295. return min(rounded, maxBolus)
  296. }
  297. private var bolusReporter: DoseProgressReporter?
  298. func enactBolus(amount: Double, isSMB: Bool) {
  299. if let error = verifyStatus() {
  300. processError(error)
  301. processQueue.async {
  302. self.broadcaster.notify(BolusFailureObserver.self, on: self.processQueue) {
  303. $0.bolusDidFail()
  304. }
  305. }
  306. return
  307. }
  308. guard let pump = pumpManager else { return }
  309. let roundedAmout = pump.roundToSupportedBolusVolume(units: amount)
  310. debug(.apsManager, "Enact bolus \(roundedAmout), manual \(!isSMB)")
  311. pump.enactBolus(units: roundedAmout, automatic: isSMB).sink { completion in
  312. if case let .failure(error) = completion {
  313. warning(.apsManager, "Bolus failed with error: \(error.localizedDescription)")
  314. self.processError(APSError.pumpError(error))
  315. if !isSMB {
  316. self.processQueue.async {
  317. self.broadcaster.notify(BolusFailureObserver.self, on: self.processQueue) {
  318. $0.bolusDidFail()
  319. }
  320. }
  321. }
  322. } else {
  323. debug(.apsManager, "Bolus succeeded")
  324. if !isSMB {
  325. self.determineBasal().sink { _ in }.store(in: &self.lifetime)
  326. }
  327. self.bolusProgress.send(0)
  328. }
  329. } receiveValue: { _ in }
  330. .store(in: &lifetime)
  331. }
  332. func cancelBolus() {
  333. guard let pump = pumpManager, pump.status.pumpStatus.bolusing else { return }
  334. debug(.apsManager, "Cancel bolus")
  335. pump.cancelBolus().sink { completion in
  336. if case let .failure(error) = completion {
  337. debug(.apsManager, "Bolus cancellation failed with error: \(error.localizedDescription)")
  338. self.processError(APSError.pumpError(error))
  339. } else {
  340. debug(.apsManager, "Bolus cancelled")
  341. }
  342. self.bolusReporter?.removeObserver(self)
  343. self.bolusReporter = nil
  344. self.bolusProgress.send(nil)
  345. } receiveValue: { _ in }
  346. .store(in: &lifetime)
  347. }
  348. func enactTempBasal(rate: Double, duration: TimeInterval) {
  349. if let error = verifyStatus() {
  350. processError(error)
  351. return
  352. }
  353. guard let pump = pumpManager else { return }
  354. // unable to do temp basal during manual temp basal 😁
  355. if isManualTempBasal {
  356. processError(APSError.manualBasalTemp(message: "Loop not possible during the manual basal temp"))
  357. return
  358. }
  359. debug(.apsManager, "Enact temp basal \(rate) - \(duration)")
  360. let roundedAmout = pump.roundToSupportedBasalRate(unitsPerHour: rate)
  361. pump.enactTempBasal(unitsPerHour: roundedAmout, for: duration) { error in
  362. if let error = error {
  363. debug(.apsManager, "Temp Basal failed with error: \(error.localizedDescription)")
  364. self.processError(APSError.pumpError(error))
  365. } else {
  366. debug(.apsManager, "Temp Basal succeeded")
  367. let temp = TempBasal(duration: Int(duration / 60), rate: Decimal(rate), temp: .absolute, timestamp: Date())
  368. self.storage.save(temp, as: OpenAPS.Monitor.tempBasal)
  369. if rate == 0, duration == 0 {
  370. self.pumpHistoryStorage.saveCancelTempEvents()
  371. }
  372. }
  373. }
  374. }
  375. func dailyAutotune() -> AnyPublisher<Bool, Never> {
  376. guard settings.useAutotune else {
  377. return Just(false).eraseToAnyPublisher()
  378. }
  379. let now = Date()
  380. guard lastAutotuneDate.isBeforeDate(now, granularity: .day) else {
  381. return Just(false).eraseToAnyPublisher()
  382. }
  383. lastAutotuneDate = now
  384. return autotune().map { $0 != nil }.eraseToAnyPublisher()
  385. }
  386. func autotune() -> AnyPublisher<Autotune?, Never> {
  387. openAPS.autotune().eraseToAnyPublisher()
  388. }
  389. func enactAnnouncement(_ announcement: Announcement) {
  390. guard let action = announcement.action else {
  391. warning(.apsManager, "Invalid Announcement action")
  392. return
  393. }
  394. guard let pump = pumpManager else {
  395. warning(.apsManager, "Pump is not set")
  396. return
  397. }
  398. debug(.apsManager, "Start enact announcement: \(action)")
  399. switch action {
  400. case let .bolus(amount):
  401. if let error = verifyStatus() {
  402. processError(error)
  403. return
  404. }
  405. let roundedAmount = pump.roundToSupportedBolusVolume(units: Double(amount))
  406. pump.enactBolus(units: roundedAmount, activationType: .manualRecommendationAccepted) { error in
  407. if let error = error {
  408. // warning(.apsManager, "Announcement Bolus failed with error: \(error.localizedDescription)")
  409. switch error {
  410. case .uncertainDelivery:
  411. // Do not generate notification on uncertain delivery error
  412. break
  413. default:
  414. // Do not generate notifications for automatic boluses that fail.
  415. warning(.apsManager, "Announcement Bolus failed with error: \(error.localizedDescription)")
  416. }
  417. } else {
  418. debug(.apsManager, "Announcement Bolus succeeded")
  419. self.announcementsStorage.storeAnnouncements([announcement], enacted: true)
  420. self.bolusProgress.send(0)
  421. }
  422. }
  423. case let .pump(pumpAction):
  424. switch pumpAction {
  425. case .suspend:
  426. if let error = verifyStatus() {
  427. processError(error)
  428. return
  429. }
  430. pump.suspendDelivery { error in
  431. if let error = error {
  432. debug(.apsManager, "Pump not suspended by Announcement: \(error.localizedDescription)")
  433. } else {
  434. debug(.apsManager, "Pump suspended by Announcement")
  435. self.announcementsStorage.storeAnnouncements([announcement], enacted: true)
  436. self.nightscout.uploadStatus()
  437. }
  438. }
  439. case .resume:
  440. guard pump.status.pumpStatus.suspended else {
  441. return
  442. }
  443. pump.resumeDelivery { error in
  444. if let error = error {
  445. warning(.apsManager, "Pump not resumed by Announcement: \(error.localizedDescription)")
  446. } else {
  447. debug(.apsManager, "Pump resumed by Announcement")
  448. self.announcementsStorage.storeAnnouncements([announcement], enacted: true)
  449. self.nightscout.uploadStatus()
  450. }
  451. }
  452. }
  453. case let .looping(closedLoop):
  454. settings.closedLoop = closedLoop
  455. debug(.apsManager, "Closed loop \(closedLoop) by Announcement")
  456. announcementsStorage.storeAnnouncements([announcement], enacted: true)
  457. case let .tempbasal(rate, duration):
  458. if let error = verifyStatus() {
  459. processError(error)
  460. return
  461. }
  462. // unable to do temp basal during manual temp basal 😁
  463. if isManualTempBasal {
  464. processError(APSError.manualBasalTemp(message: "Loop not possible during the manual basal temp"))
  465. return
  466. }
  467. guard !settings.closedLoop else {
  468. return
  469. }
  470. let roundedRate = pump.roundToSupportedBasalRate(unitsPerHour: Double(rate))
  471. pump.enactTempBasal(unitsPerHour: roundedRate, for: TimeInterval(duration) * 60) { error in
  472. if let error = error {
  473. warning(.apsManager, "Announcement TempBasal failed with error: \(error.localizedDescription)")
  474. } else {
  475. debug(.apsManager, "Announcement TempBasal succeeded")
  476. self.announcementsStorage.storeAnnouncements([announcement], enacted: true)
  477. }
  478. }
  479. }
  480. }
  481. private func currentTemp(date: Date) -> TempBasal {
  482. let defaultTemp = { () -> TempBasal in
  483. guard let temp = storage.retrieve(OpenAPS.Monitor.tempBasal, as: TempBasal.self) else {
  484. return TempBasal(duration: 0, rate: 0, temp: .absolute, timestamp: Date())
  485. }
  486. let delta = Int((date.timeIntervalSince1970 - temp.timestamp.timeIntervalSince1970) / 60)
  487. let duration = max(0, temp.duration - delta)
  488. return TempBasal(duration: duration, rate: temp.rate, temp: .absolute, timestamp: date)
  489. }()
  490. guard let state = pumpManager?.status.basalDeliveryState else { return defaultTemp }
  491. switch state {
  492. case .active:
  493. return TempBasal(duration: 0, rate: 0, temp: .absolute, timestamp: date)
  494. case let .tempBasal(dose):
  495. let rate = Decimal(dose.unitsPerHour)
  496. let durationMin = max(0, Int((dose.endDate.timeIntervalSince1970 - date.timeIntervalSince1970) / 60))
  497. return TempBasal(duration: durationMin, rate: rate, temp: .absolute, timestamp: date)
  498. default:
  499. return defaultTemp
  500. }
  501. }
  502. private func enactSuggested() -> AnyPublisher<Void, Error> {
  503. guard let suggested = storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self) else {
  504. return Fail(error: APSError.apsError(message: "Suggestion not found")).eraseToAnyPublisher()
  505. }
  506. guard Date().timeIntervalSince(suggested.deliverAt ?? .distantPast) < Config.eхpirationInterval else {
  507. return Fail(error: APSError.apsError(message: "Suggestion expired")).eraseToAnyPublisher()
  508. }
  509. guard let pump = pumpManager else {
  510. return Fail(error: APSError.apsError(message: "Pump not set")).eraseToAnyPublisher()
  511. }
  512. // unable to do temp basal during manual temp basal 😁
  513. if isManualTempBasal {
  514. return Fail(error: APSError.manualBasalTemp(message: "Loop not possible during the manual basal temp"))
  515. .eraseToAnyPublisher()
  516. }
  517. let basalPublisher: AnyPublisher<Void, Error> = Deferred { () -> AnyPublisher<Void, Error> in
  518. if let error = self.verifyStatus() {
  519. return Fail(error: error).eraseToAnyPublisher()
  520. }
  521. guard let rate = suggested.rate, let duration = suggested.duration else {
  522. // It is OK, no temp required
  523. debug(.apsManager, "No temp required")
  524. return Just(()).setFailureType(to: Error.self)
  525. .eraseToAnyPublisher()
  526. }
  527. return pump.enactTempBasal(unitsPerHour: Double(rate), for: TimeInterval(duration * 60)).map { _ in
  528. let temp = TempBasal(duration: duration, rate: rate, temp: .absolute, timestamp: Date())
  529. self.storage.save(temp, as: OpenAPS.Monitor.tempBasal)
  530. return ()
  531. }
  532. .eraseToAnyPublisher()
  533. }.eraseToAnyPublisher()
  534. let bolusPublisher: AnyPublisher<Void, Error> = Deferred { () -> AnyPublisher<Void, Error> in
  535. if let error = self.verifyStatus() {
  536. return Fail(error: error).eraseToAnyPublisher()
  537. }
  538. guard let units = suggested.units else {
  539. // It is OK, no bolus required
  540. debug(.apsManager, "No bolus required")
  541. return Just(()).setFailureType(to: Error.self)
  542. .eraseToAnyPublisher()
  543. }
  544. return pump.enactBolus(units: Double(units), automatic: true).map { _ in
  545. self.bolusProgress.send(0)
  546. return ()
  547. }
  548. .eraseToAnyPublisher()
  549. }.eraseToAnyPublisher()
  550. return basalPublisher.flatMap { bolusPublisher }.eraseToAnyPublisher()
  551. }
  552. private func reportEnacted(received: Bool) {
  553. if let suggestion = storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self), suggestion.deliverAt != nil {
  554. var enacted = suggestion
  555. enacted.timestamp = Date()
  556. enacted.recieved = received
  557. storage.save(enacted, as: OpenAPS.Enact.enacted)
  558. // Add to tdd.json:
  559. //
  560. let preferences = settingsManager.preferences
  561. let currentTDD = enacted.tdd ?? 0
  562. let file = OpenAPS.Monitor.tdd
  563. let tdd = TDD(
  564. TDD: currentTDD,
  565. timestamp: Date(),
  566. id: UUID().uuidString
  567. )
  568. var uniqEvents: [TDD] = []
  569. storage.transaction { storage in
  570. storage.append(tdd, to: file, uniqBy: \.id)
  571. uniqEvents = storage.retrieve(file, as: [TDD].self)?
  572. .filter { $0.timestamp.addingTimeInterval(7.days.timeInterval) > Date() }
  573. .sorted { $0.timestamp > $1.timestamp } ?? []
  574. var total: Decimal = 0
  575. var indeces: Decimal = 0
  576. for uniqEvent in uniqEvents {
  577. if uniqEvent.TDD > 0 {
  578. total += uniqEvent.TDD
  579. indeces += 1
  580. }
  581. }
  582. let entriesPast2hours = storage.retrieve(file, as: [TDD].self)?
  583. .filter { $0.timestamp.addingTimeInterval(2.hours.timeInterval) > Date() }
  584. .sorted { $0.timestamp > $1.timestamp } ?? []
  585. var totalAmount: Decimal = 0
  586. var nrOfIndeces: Decimal = 0
  587. for entry in entriesPast2hours {
  588. if entry.TDD > 0 {
  589. totalAmount += entry.TDD
  590. nrOfIndeces += 1
  591. }
  592. }
  593. if indeces == 0 {
  594. indeces = 1
  595. }
  596. if nrOfIndeces == 0 {
  597. nrOfIndeces = 1
  598. }
  599. let average7 = total / indeces
  600. let average2hours = totalAmount / nrOfIndeces
  601. let weight = preferences.weightPercentage
  602. let weighted_average = weight * average2hours + (1 - weight) * average7
  603. let averages = TDD_averages(
  604. average_7days: average7,
  605. weightedAverage: weighted_average,
  606. past2hoursAverage: average2hours,
  607. date: Date()
  608. )
  609. storage.save(averages, as: OpenAPS.Monitor.tdd_averages)
  610. storage.save(Array(uniqEvents), as: file)
  611. }
  612. // End of tdd.json
  613. debug(.apsManager, "Suggestion enacted. Received: \(received)")
  614. DispatchQueue.main.async {
  615. self.broadcaster.notify(EnactedSuggestionObserver.self, on: .main) {
  616. $0.enactedSuggestionDidUpdate(enacted)
  617. }
  618. }
  619. nightscout.uploadStatus()
  620. }
  621. }
  622. private func processError(_ error: Error) {
  623. warning(.apsManager, "\(error.localizedDescription)")
  624. lastError.send(error)
  625. }
  626. private func createBolusReporter() {
  627. bolusReporter = pumpManager?.createBolusProgressReporter(reportingOn: processQueue)
  628. bolusReporter?.addObserver(self)
  629. }
  630. private func updateStatus() {
  631. debug(.apsManager, "force update status")
  632. guard let pump = pumpManager else {
  633. return
  634. }
  635. if let omnipod = pump as? OmnipodPumpManager {
  636. omnipod.getPodStatus { _ in }
  637. }
  638. if let omnipodBLE = pump as? OmniBLEPumpManager {
  639. omnipodBLE.getPodStatus { _ in }
  640. }
  641. }
  642. private func clearBolusReporter() {
  643. bolusReporter?.removeObserver(self)
  644. bolusReporter = nil
  645. processQueue.asyncAfter(deadline: .now() + 0.5) {
  646. self.bolusProgress.send(nil)
  647. self.updateStatus()
  648. }
  649. }
  650. }
  651. private extension PumpManager {
  652. func enactTempBasal(unitsPerHour: Double, for duration: TimeInterval) -> AnyPublisher<DoseEntry?, Error> {
  653. Future { promise in
  654. self.enactTempBasal(unitsPerHour: unitsPerHour, for: duration) { error in
  655. if let error = error {
  656. debug(.apsManager, "Temp basal failed: \(unitsPerHour) for: \(duration)")
  657. promise(.failure(error))
  658. } else {
  659. debug(.apsManager, "Temp basal succeded: \(unitsPerHour) for: \(duration)")
  660. promise(.success(nil))
  661. }
  662. }
  663. }
  664. .mapError { APSError.pumpError($0) }
  665. .eraseToAnyPublisher()
  666. }
  667. func enactBolus(units: Double, automatic: Bool) -> AnyPublisher<DoseEntry?, Error> {
  668. Future { promise in
  669. // convert automatic
  670. let automaticValue = automatic ? BolusActivationType.automatic : BolusActivationType.manualRecommendationAccepted
  671. self.enactBolus(units: units, activationType: automaticValue) { error in
  672. if let error = error {
  673. debug(.apsManager, "Bolus failed: \(units)")
  674. promise(.failure(error))
  675. } else {
  676. debug(.apsManager, "Bolus succeded: \(units)")
  677. promise(.success(nil))
  678. }
  679. }
  680. }
  681. .mapError { APSError.pumpError($0) }
  682. .eraseToAnyPublisher()
  683. }
  684. func cancelBolus() -> AnyPublisher<DoseEntry?, Error> {
  685. Future { promise in
  686. self.cancelBolus { result in
  687. switch result {
  688. case let .success(dose):
  689. debug(.apsManager, "Cancel Bolus succeded")
  690. promise(.success(dose))
  691. case let .failure(error):
  692. debug(.apsManager, "Cancel Bolus failed")
  693. promise(.failure(error))
  694. }
  695. }
  696. }
  697. .mapError { APSError.pumpError($0) }
  698. .eraseToAnyPublisher()
  699. }
  700. func suspendDelivery() -> AnyPublisher<Void, Error> {
  701. Future { promise in
  702. self.suspendDelivery { error in
  703. if let error = error {
  704. promise(.failure(error))
  705. } else {
  706. promise(.success(()))
  707. }
  708. }
  709. }
  710. .mapError { APSError.pumpError($0) }
  711. .eraseToAnyPublisher()
  712. }
  713. func resumeDelivery() -> AnyPublisher<Void, Error> {
  714. Future { promise in
  715. self.resumeDelivery { error in
  716. if let error = error {
  717. promise(.failure(error))
  718. } else {
  719. promise(.success(()))
  720. }
  721. }
  722. }
  723. .mapError { APSError.pumpError($0) }
  724. .eraseToAnyPublisher()
  725. }
  726. }
  727. extension BaseAPSManager: PumpManagerStatusObserver {
  728. func pumpManager(_: PumpManager, didUpdate status: PumpManagerStatus, oldStatus _: PumpManagerStatus) {
  729. let percent = Int((status.pumpBatteryChargeRemaining ?? 1) * 100)
  730. let battery = Battery(
  731. percent: percent,
  732. voltage: nil,
  733. string: percent > 10 ? .normal : .low,
  734. display: status.pumpBatteryChargeRemaining != nil
  735. )
  736. storage.save(battery, as: OpenAPS.Monitor.battery)
  737. storage.save(status.pumpStatus, as: OpenAPS.Monitor.status)
  738. }
  739. }
  740. extension BaseAPSManager: DoseProgressObserver {
  741. func doseProgressReporterDidUpdate(_ doseProgressReporter: DoseProgressReporter) {
  742. bolusProgress.send(Decimal(doseProgressReporter.progress.percentComplete))
  743. if doseProgressReporter.progress.isComplete {
  744. clearBolusReporter()
  745. }
  746. }
  747. }
  748. extension PumpManagerStatus {
  749. var pumpStatus: PumpStatus {
  750. let bolusing = bolusState != .noBolus
  751. let suspended = basalDeliveryState?.isSuspended ?? true
  752. let type = suspended ? StatusType.suspended : (bolusing ? .bolusing : .normal)
  753. return PumpStatus(status: type, bolusing: bolusing, suspended: suspended, timestamp: Date())
  754. }
  755. }