MinimedPumpSettingsViewModel.swift 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. //
  2. // MinimedPumpSettingsViewModel.swift
  3. // MinimedKitUI
  4. //
  5. // Created by Pete Schwamb on 11/29/22.
  6. // Copyright © 2022 LoopKit Authors. All rights reserved.
  7. //
  8. import Foundation
  9. import MinimedKit
  10. import LoopKit
  11. import SwiftUI
  12. import LoopKitUI
  13. import HealthKit
  14. enum MinimedSettingsViewAlert: Identifiable {
  15. case suspendError(Error)
  16. case resumeError(Error)
  17. case syncTimeError(PumpManagerError)
  18. var id: String {
  19. switch self {
  20. case .suspendError:
  21. return "suspendError"
  22. case .resumeError:
  23. return "resumeError"
  24. case .syncTimeError:
  25. return "syncTimeError"
  26. }
  27. }
  28. }
  29. enum MySentryConfig: Int, Identifiable, CaseIterable {
  30. var id: RawValue {
  31. return self.rawValue
  32. }
  33. case useMySentry = 0
  34. case doNotUseMySentry
  35. var localizedDescription: String {
  36. switch self {
  37. case .useMySentry:
  38. return LocalizedString("Use MySentry", comment: "Description for option to use MySentry")
  39. case .doNotUseMySentry:
  40. return LocalizedString("Do not use MySentry", comment: "Description for option to not use MySentry")
  41. }
  42. }
  43. }
  44. public enum ReservoirLevelHighlightState: String, Equatable {
  45. case normal
  46. case warning
  47. case critical
  48. }
  49. class MinimedPumpSettingsViewModel: ObservableObject {
  50. @Published var suspendResumeTransitioning: Bool = false
  51. @Published var basalDeliveryState: PumpManagerStatus.BasalDeliveryState?
  52. @Published var reservoirReading: ReservoirReading?
  53. @Published var batteryChemistryType: BatteryChemistryType {
  54. didSet {
  55. pumpManager.batteryChemistry = batteryChemistryType
  56. }
  57. }
  58. @Published var preferredDataSource: InsulinDataSource {
  59. didSet {
  60. pumpManager.preferredInsulinDataSource = preferredDataSource
  61. }
  62. }
  63. @Published var mySentryConfig: MySentryConfig {
  64. didSet {
  65. pumpManager.useMySentry = mySentryConfig == .useMySentry
  66. }
  67. }
  68. @Published var activeAlert: MinimedSettingsViewAlert?
  69. @Published var suspendResumeButtonEnabled: Bool = false
  70. @Published var synchronizingTime: Bool = false
  71. var pumpManager: MinimedPumpManager
  72. var didFinish: (() -> Void)?
  73. let basalRateFormatter: NumberFormatter = {
  74. let numberFormatter = NumberFormatter()
  75. numberFormatter.numberStyle = .decimal
  76. numberFormatter.minimumFractionDigits = 1
  77. numberFormatter.minimumIntegerDigits = 1
  78. return numberFormatter
  79. }()
  80. let reservoirVolumeFormatter = {
  81. let formatter = QuantityFormatter(for: .internationalUnit())
  82. formatter.numberFormatter.maximumFractionDigits = 1
  83. return formatter
  84. }()
  85. init(pumpManager: MinimedPumpManager) {
  86. self.pumpManager = pumpManager
  87. self.basalDeliveryState = pumpManager.status.basalDeliveryState
  88. self.reservoirReading = pumpManager.state.lastReservoirReading
  89. self.batteryChemistryType = pumpManager.batteryChemistry
  90. self.preferredDataSource = pumpManager.preferredInsulinDataSource
  91. self.mySentryConfig = pumpManager.useMySentry ? .useMySentry : .doNotUseMySentry
  92. self.pumpManager.addStatusObserver(self, queue: DispatchQueue.main)
  93. pumpManager.stateObservers.insert(self, queue: .main)
  94. }
  95. var pumpImage: UIImage {
  96. return pumpManager.state.largePumpImage
  97. }
  98. func deletePump() {
  99. pumpManager.deletePump {
  100. DispatchQueue.main.async {
  101. self.didFinish?()
  102. }
  103. }
  104. }
  105. func doneButtonPressed() {
  106. self.didFinish?()
  107. }
  108. func suspendResumeButtonPressed(action: SuspendResumeAction) {
  109. suspendResumeButtonEnabled = true
  110. switch action {
  111. case .resume:
  112. pumpManager.resumeDelivery { error in
  113. DispatchQueue.main.async {
  114. self.suspendResumeButtonEnabled = false
  115. if let error = error {
  116. self.activeAlert = .resumeError(error)
  117. }
  118. }
  119. }
  120. case .suspend:
  121. pumpManager.suspendDelivery { error in
  122. DispatchQueue.main.async {
  123. self.suspendResumeButtonEnabled = false
  124. if let error = error {
  125. self.activeAlert = .suspendError(error)
  126. }
  127. }
  128. }
  129. }
  130. }
  131. func didChangeInsulinType(_ newType: InsulinType?) {
  132. self.pumpManager.insulinType = newType
  133. }
  134. var isScheduledBasal: Bool {
  135. switch basalDeliveryState {
  136. case .active(_), .initiatingTempBasal:
  137. return true
  138. case .tempBasal(_), .cancelingTempBasal, .suspending, .suspended(_), .resuming, .none:
  139. return false
  140. }
  141. }
  142. var isSuspendedOrResuming: Bool {
  143. switch basalDeliveryState {
  144. case .suspended, .resuming:
  145. return true
  146. default:
  147. return false
  148. }
  149. }
  150. func suspendResumeButtonColor(guidanceColors: GuidanceColors) -> Color {
  151. switch basalDeliveryState {
  152. case .suspending, .resuming:
  153. return Color.secondary
  154. case .suspended:
  155. return guidanceColors.warning
  156. default:
  157. return .accentColor
  158. }
  159. }
  160. var basalDeliveryRate: Double? {
  161. switch basalDeliveryState {
  162. case .suspending, .resuming, .suspended, .none, .initiatingTempBasal, .cancelingTempBasal:
  163. return nil
  164. case .active:
  165. // return scheduled basal rate
  166. var calendar = Calendar(identifier: .gregorian)
  167. calendar.timeZone = pumpManager.state.timeZone
  168. return pumpManager.state.basalSchedule.currentRate(using: calendar)
  169. case .tempBasal(let dose):
  170. return dose.unitsPerHour
  171. }
  172. }
  173. public var reservoirLevelHighlightState: ReservoirLevelHighlightState? {
  174. guard let reservoirReading = reservoirReading else {
  175. return nil
  176. }
  177. let value = reservoirReading.units
  178. if value > pumpManager.lowReservoirWarningLevel {
  179. return .normal
  180. } else if value > 0 {
  181. return .warning
  182. } else {
  183. return .critical
  184. }
  185. }
  186. public var reservoirPercentage: Double? {
  187. guard let reservoirReading = reservoirReading else {
  188. return nil
  189. }
  190. return (reservoirReading.units / pumpManager.pumpReservoirCapacity).clamped(to: 0...1.0)
  191. }
  192. func reservoirText(for units: Double) -> String {
  193. let quantity = HKQuantity(unit: .internationalUnit(), doubleValue: units)
  194. return reservoirVolumeFormatter.string(from: quantity, for: .internationalUnit()) ?? ""
  195. }
  196. var isClockOffset: Bool {
  197. return pumpManager.isClockOffset
  198. }
  199. func changeTimeZoneTapped() {
  200. synchronizingTime = true
  201. pumpManager.setTime { (error) in
  202. DispatchQueue.main.async {
  203. self.synchronizingTime = false
  204. if let error = error {
  205. self.activeAlert = .syncTimeError(error)
  206. }
  207. }
  208. }
  209. }
  210. }
  211. extension MinimedPumpSettingsViewModel: PumpManagerStatusObserver {
  212. public func pumpManager(_ pumpManager: PumpManager, didUpdate status: PumpManagerStatus, oldStatus: PumpManagerStatus) {
  213. basalDeliveryState = status.basalDeliveryState
  214. }
  215. }
  216. extension MinimedPumpSettingsViewModel: MinimedPumpManagerStateObserver {
  217. func didUpdatePumpManagerState(_ state: MinimedKit.MinimedPumpManagerState) {
  218. reservoirReading = state.lastReservoirReading
  219. batteryChemistryType = state.batteryChemistry
  220. preferredDataSource = state.preferredInsulinDataSource
  221. mySentryConfig = state.useMySentry ? .useMySentry : .doNotUseMySentry
  222. }
  223. }
  224. enum SuspendResumeAction {
  225. case suspend
  226. case resume
  227. }
  228. extension PumpManagerStatus.BasalDeliveryState {
  229. var shownAction: SuspendResumeAction {
  230. switch self {
  231. case .active, .suspending, .tempBasal, .cancelingTempBasal, .initiatingTempBasal:
  232. return .suspend
  233. case .suspended, .resuming:
  234. return .resume
  235. }
  236. }
  237. var buttonLabelText: String {
  238. switch self {
  239. case .active, .tempBasal:
  240. return LocalizedString("Suspend Delivery", comment: "Title text for button to suspend insulin delivery")
  241. case .suspending:
  242. return LocalizedString("Suspending", comment: "Title text for button when insulin delivery is in the process of being stopped")
  243. case .suspended:
  244. return LocalizedString("Resume Delivery", comment: "Title text for button to resume insulin delivery")
  245. case .resuming:
  246. return LocalizedString("Resuming", comment: "Title text for button when insulin delivery is in the process of being resumed")
  247. case .initiatingTempBasal:
  248. return LocalizedString("Starting Temp Basal", comment: "Title text for suspend resume button when temp basal starting")
  249. case .cancelingTempBasal:
  250. return LocalizedString("Canceling Temp Basal", comment: "Title text for suspend resume button when temp basal canceling")
  251. }
  252. }
  253. var isTransitioning: Bool {
  254. switch self {
  255. case .suspending, .resuming, .initiatingTempBasal, .cancelingTempBasal:
  256. return true
  257. default:
  258. return false
  259. }
  260. }
  261. }