APSManager.swift 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. import Combine
  2. import Foundation
  3. import LoopKit
  4. import LoopKitUI
  5. import Swinject
  6. protocol APSManager {
  7. func fetchAndLoop()
  8. func autosense()
  9. func autotune()
  10. func enactBolus(amount: Double)
  11. var pumpManager: PumpManagerUI? { get set }
  12. var pumpDisplayState: CurrentValueSubject<PumpDisplayState?, Never> { get }
  13. }
  14. final class BaseAPSManager: APSManager, Injectable {
  15. @Injected() private var storage: FileStorage!
  16. @Injected() private var pumpHistoryStorage: PumpHistoryStorage!
  17. @Injected() private var glucoseStorage: GlucoseStorage!
  18. @Injected() private var tempTargetsStorage: TempTargetsStorage!
  19. @Injected() private var carbsStorage: CarbsStorage!
  20. @Injected() private var announcementsStorage: AnnouncementsStorage!
  21. @Injected() private var deviceDataManager: DeviceDataManager!
  22. @Injected() private var nightscout: NightscoutManager!
  23. @Injected() private var settingsManager: SettingsManager!
  24. @Injected() private var broadcaster: Broadcaster!
  25. private var openAPS: OpenAPS!
  26. private var loopCancellable: AnyCancellable?
  27. private var pumpCancellable: AnyCancellable?
  28. private var enactCancellable: AnyCancellable?
  29. private var remoteCancellable: AnyCancellable?
  30. var pumpManager: PumpManagerUI? {
  31. get { deviceDataManager.pumpManager }
  32. set { deviceDataManager.pumpManager = newValue }
  33. }
  34. var pumpDisplayState: CurrentValueSubject<PumpDisplayState?, Never> {
  35. deviceDataManager.pumpDisplayState
  36. }
  37. var settings: FreeAPSSettings {
  38. get { settingsManager.settings }
  39. set { settingsManager.settings = newValue }
  40. }
  41. init(resolver: Resolver) {
  42. injectServices(resolver)
  43. openAPS = OpenAPS(storage: storage)
  44. subscribe()
  45. }
  46. private func subscribe() {
  47. pumpCancellable = deviceDataManager.recommendsLoop
  48. .sink { [weak self] in
  49. self?.fetchAndLoop()
  50. }
  51. }
  52. func fetchAndLoop() {
  53. guard pumpManager != nil else {
  54. loop()
  55. return
  56. }
  57. remoteCancellable = nightscout.fetchAnnouncements()
  58. .sink { [weak self] in
  59. if let recent = self?.announcementsStorage.recent(), recent.action != nil {
  60. self?.enactAnnouncement(recent)
  61. } else {
  62. self?.loop()
  63. }
  64. }
  65. }
  66. private func loop() {
  67. loopCancellable = Publishers.CombineLatest3(
  68. nightscout.fetchGlucose(),
  69. nightscout.fetchCarbs(),
  70. nightscout.fetchTempTargets()
  71. )
  72. .flatMap { _ in self.determineBasal() }
  73. .sink { _ in } receiveValue: { [weak self] ok in
  74. guard let self = self else { return }
  75. if ok, let suggested = try? self.storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self) {
  76. DispatchQueue.main.async {
  77. self.broadcaster.notify(SuggestionObserver.self, on: .main) {
  78. $0.suggestionDidUpdate(suggested)
  79. }
  80. }
  81. }
  82. if ok, self.settings.closedLoop {
  83. self.enactSuggested()
  84. }
  85. }
  86. }
  87. private func determineBasal() -> AnyPublisher<Bool, Never> {
  88. guard let glucose = try? storage.retrieve(OpenAPS.Monitor.glucose, as: [BloodGlucose].self), glucose.count >= 36 else {
  89. print("Not enough glucose data")
  90. return Just(false).eraseToAnyPublisher()
  91. }
  92. let now = Date()
  93. guard let temp = currentTemp(date: now) else {
  94. return Just(false).eraseToAnyPublisher()
  95. }
  96. return openAPS.makeProfiles()
  97. .flatMap { _ in
  98. self.openAPS.determineBasal(currentTemp: temp, clock: now)
  99. }
  100. .map { true }
  101. .eraseToAnyPublisher()
  102. }
  103. func enactBolus(amount: Double) {
  104. guard let pump = pumpManager else { return }
  105. let roundedAmout = pump.roundToSupportedBolusVolume(units: amount)
  106. pump.enactBolus(units: roundedAmout, automatic: false) { result in
  107. switch result {
  108. case .success:
  109. print("Bolus succeeded")
  110. case let .failure(error):
  111. print("Bolus failed with error: \(error.localizedDescription)")
  112. }
  113. }
  114. }
  115. func autosense() {
  116. _ = openAPS.autosense()
  117. }
  118. func autotune() {
  119. _ = openAPS.autotune()
  120. }
  121. private func enactAnnouncement(_ announcement: Announcement) {
  122. guard let action = announcement.action else {
  123. print("Invalid Announcement action")
  124. return
  125. }
  126. switch action {
  127. case let .bolus(amount):
  128. pumpManager?.enactBolus(units: Double(amount), automatic: false) { result in
  129. switch result {
  130. case .success:
  131. print("Announcement Bolus succeeded")
  132. self.announcementsStorage.storeAnnouncements([announcement], enacted: true)
  133. case let .failure(error):
  134. print("Announcement Bolus failed with error: \(error.localizedDescription)")
  135. }
  136. }
  137. case let .pump(pumpAction):
  138. switch pumpAction {
  139. case .suspend:
  140. pumpManager?.suspendDelivery { error in
  141. if let error = error {
  142. print("Pump not suspended by Announcement: \(error.localizedDescription)")
  143. } else {
  144. print("Pump suspended by Announcement")
  145. self.announcementsStorage.storeAnnouncements([announcement], enacted: true)
  146. }
  147. }
  148. case .resume:
  149. pumpManager?.resumeDelivery { error in
  150. if let error = error {
  151. print("Pump not resumed by Announcement: \(error.localizedDescription)")
  152. } else {
  153. print("Pump resumed by Announcement")
  154. self.announcementsStorage.storeAnnouncements([announcement], enacted: true)
  155. }
  156. }
  157. }
  158. case let .looping(closedLoop):
  159. settings.closedLoop = closedLoop
  160. print("Closed loop \(closedLoop) by Announcement")
  161. announcementsStorage.storeAnnouncements([announcement], enacted: true)
  162. case let .tempbasal(rate, duration):
  163. pumpManager?.enactTempBasal(unitsPerHour: Double(rate), for: TimeInterval(duration) * 60) { result in
  164. switch result {
  165. case .success:
  166. print("Announcement TempBasal succeeded")
  167. self.announcementsStorage.storeAnnouncements([announcement], enacted: true)
  168. case let .failure(error):
  169. print("Announcement TempBasal failed with error: \(error.localizedDescription)")
  170. }
  171. }
  172. }
  173. }
  174. private func currentTemp(date: Date) -> TempBasal? {
  175. guard let state = pumpManager?.status.basalDeliveryState else { return nil }
  176. switch state {
  177. case .active:
  178. return TempBasal(duration: 0, rate: 0, temp: .absolute)
  179. case let .tempBasal(dose):
  180. let rate = Decimal(dose.unitsPerHour)
  181. let durationMin = max(0, Int((dose.endDate.timeIntervalSince1970 - date.timeIntervalSince1970) / 60))
  182. return TempBasal(duration: durationMin, rate: rate, temp: .absolute)
  183. default: return nil
  184. }
  185. }
  186. private func enactSuggested() {
  187. guard let pump = pumpManager,
  188. let suggested = try? storage.retrieve(
  189. OpenAPS.Enact.suggested,
  190. as: Suggestion.self
  191. )
  192. else {
  193. return
  194. }
  195. let basalPublisher: AnyPublisher<Void, Error> = {
  196. guard let rate = suggested.rate, let duration = suggested.duration else {
  197. return Just(()).setFailureType(to: Error.self)
  198. .eraseToAnyPublisher()
  199. }
  200. return pump.enactTempBasal(unitsPerHour: Double(rate), for: TimeInterval(duration * 60)).map { _ in () }
  201. .eraseToAnyPublisher()
  202. }()
  203. let bolusPublisher: AnyPublisher<Void, Error> = {
  204. guard let units = suggested.units else {
  205. return Just(()).setFailureType(to: Error.self)
  206. .eraseToAnyPublisher()
  207. }
  208. return pump.enactBolus(units: Double(units), automatic: true).map { _ in () }
  209. .eraseToAnyPublisher()
  210. }()
  211. enactCancellable = basalPublisher
  212. .flatMap { bolusPublisher }
  213. .sink { completion in
  214. if case let .failure(error) = completion {
  215. print("Loop failed with error: \(error.localizedDescription)")
  216. }
  217. } receiveValue: { [weak self] in
  218. print("Loop succeeded")
  219. if let rawSuggested = self?.storage.retrieveRaw(OpenAPS.Enact.suggested) {
  220. try? self?.storage.save(rawSuggested, as: OpenAPS.Enact.enacted)
  221. }
  222. }
  223. }
  224. }
  225. private extension PumpManager {
  226. func enactTempBasal(unitsPerHour: Double, for duration: TimeInterval) -> AnyPublisher<DoseEntry, Error> {
  227. Future { promise in
  228. self.enactTempBasal(unitsPerHour: unitsPerHour, for: duration) { result in
  229. switch result {
  230. case let .success(dose):
  231. promise(.success(dose))
  232. case let .failure(error):
  233. promise(.failure(error))
  234. }
  235. }
  236. }.eraseToAnyPublisher()
  237. }
  238. func enactBolus(units: Double, automatic: Bool) -> AnyPublisher<DoseEntry, Error> {
  239. Future { promise in
  240. self.enactBolus(units: units, automatic: automatic) { result in
  241. switch result {
  242. case let .success(dose):
  243. promise(.success(dose))
  244. case let .failure(error):
  245. promise(.failure(error))
  246. }
  247. }
  248. }.eraseToAnyPublisher()
  249. }
  250. }