BasalProfileEditorStateModel.swift 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. import Observation
  2. import SwiftUI
  3. extension BasalProfileEditor {
  4. @Observable final class StateModel: BaseStateModel<Provider> {
  5. @ObservationIgnored @Injected() private var nightscout: NightscoutManager!
  6. @ObservationIgnored @Injected() private var tidepoolManager: TidepoolManager!
  7. @ObservationIgnored @Injected() private var broadcaster: Broadcaster!
  8. var syncInProgress: Bool = false
  9. var initialItems: [Item] = []
  10. var items: [Item] = []
  11. var therapyItems: [TherapySettingItem] = []
  12. var total: Decimal = 0.0
  13. var showAlert: Bool = false
  14. var chartData: [BasalProfile]? = []
  15. let timeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
  16. private(set) var rateValues: [Decimal] = []
  17. var canAdd: Bool {
  18. guard let lastItem = items.last else { return true }
  19. return lastItem.timeIndex < timeValues.count - 1
  20. }
  21. var hasChanges: Bool {
  22. initialItems != items
  23. }
  24. // Convert items to TherapySettingItem format
  25. func getTherapyItems() -> [TherapySettingItem] {
  26. items.map { item in
  27. TherapySettingItem(
  28. time: timeValues[item.timeIndex],
  29. value: rateValues[item.rateIndex]
  30. )
  31. }
  32. }
  33. // Update items from TherapySettingItem format
  34. func updateFromTherapyItems(_ therapyItems: [TherapySettingItem]) {
  35. items = therapyItems.map { therapyItem in
  36. let timeIndex = timeValues.firstIndex(where: { abs($0 - therapyItem.time) < 1 }) ?? 0
  37. let rateIndex = rateValues.firstIndex(of: therapyItem.value) ?? 0
  38. return Item(rateIndex: rateIndex, timeIndex: timeIndex)
  39. }
  40. }
  41. override func subscribe() {
  42. rateValues = provider.supportedBasalRates ?? stride(from: 5.0, to: 1001.0, by: 5.0)
  43. .map { ($0.decimal ?? .zero) / 100 }
  44. items = provider.profile.map { value in
  45. let timeIndex = timeValues.firstIndex(of: Double(value.minutes * 60)) ?? 0
  46. let rateIndex = rateValues.firstIndex(of: value.rate) ?? 0
  47. return Item(rateIndex: rateIndex, timeIndex: timeIndex)
  48. }
  49. initialItems = items.map { Item(rateIndex: $0.rateIndex, timeIndex: $0.timeIndex) }
  50. calcTotal()
  51. }
  52. func calcTotal() {
  53. let profile = items.map { item -> BasalProfileEntry in
  54. let fotmatter = DateFormatter()
  55. fotmatter.timeZone = TimeZone(secondsFromGMT: 0)
  56. fotmatter.dateFormat = "HH:mm:ss"
  57. let date = Date(timeIntervalSince1970: self.timeValues[item.timeIndex])
  58. let minutes = Int(date.timeIntervalSince1970 / 60)
  59. let rate = self.rateValues[item.rateIndex]
  60. return BasalProfileEntry(start: fotmatter.string(from: date), minutes: minutes, rate: rate)
  61. }
  62. var profileWith24hours = profile.map(\.minutes)
  63. profileWith24hours.append(24 * 60)
  64. let pr2 = zip(profile, profileWith24hours.dropFirst())
  65. total = pr2.reduce(0) { $0 + (Decimal($1.1 - $1.0.minutes) / 60) * $1.0.rate }
  66. }
  67. func add() {
  68. var time = 0
  69. var rate = 0
  70. if let last = items.last {
  71. time = last.timeIndex + 1
  72. rate = last.rateIndex
  73. }
  74. let newItem = Item(rateIndex: rate, timeIndex: time)
  75. items.append(newItem)
  76. calcTotal()
  77. }
  78. func save() {
  79. guard hasChanges else { return }
  80. syncInProgress = true
  81. let profile = items.map { item -> BasalProfileEntry in
  82. let formatter = DateFormatter()
  83. formatter.timeZone = TimeZone(secondsFromGMT: 0)
  84. formatter.dateFormat = "HH:mm:ss"
  85. let date = Date(timeIntervalSince1970: self.timeValues[item.timeIndex])
  86. let minutes = Int(date.timeIntervalSince1970 / 60)
  87. let rate = self.rateValues[item.rateIndex]
  88. return BasalProfileEntry(start: formatter.string(from: date), minutes: minutes, rate: rate)
  89. }
  90. provider.saveProfile(profile)
  91. .receive(on: DispatchQueue.main)
  92. .sink { completion in
  93. self.syncInProgress = false
  94. switch completion {
  95. case .finished:
  96. // Successfully saved and synced
  97. self.initialItems = self.items.map { Item(rateIndex: $0.rateIndex, timeIndex: $0.timeIndex) }
  98. DispatchQueue.main.async {
  99. self.broadcaster.notify(BasalProfileObserver.self, on: .main) {
  100. $0.basalProfileDidChange(profile)
  101. }
  102. }
  103. Task.detached(priority: .low) {
  104. do {
  105. debug(.nightscout, "Attempting to upload basal rates to Nightscout")
  106. try await self.nightscout.uploadProfiles()
  107. } catch {
  108. debug(.default, "Failed to upload basal rates to Nightscout: \(error)")
  109. }
  110. }
  111. Task.detached(priority: .low) {
  112. await self.tidepoolManager.uploadSettings()
  113. }
  114. case .failure:
  115. // Handle the error, show error message
  116. self.showAlert = true
  117. }
  118. } receiveValue: {
  119. // Handle any successful value if needed
  120. print("We were successful")
  121. }
  122. .store(in: &lifetime)
  123. }
  124. @MainActor func validate() {
  125. let uniq = Array(Set(items))
  126. let sorted = uniq.sorted { $0.timeIndex < $1.timeIndex }
  127. sorted.first?.timeIndex = 0
  128. if items != sorted {
  129. items = sorted
  130. }
  131. calcTotal()
  132. }
  133. func availableTimeIndices(_ itemIndex: Int) -> [Int] {
  134. // avoid index out of range issues
  135. guard itemIndex >= 0, itemIndex < items.count else {
  136. return []
  137. }
  138. let usedIndicesByOtherItems = items
  139. .enumerated()
  140. .filter { $0.offset != itemIndex }
  141. .map(\.element.timeIndex)
  142. return (0 ..< timeValues.count).filter { !usedIndicesByOtherItems.contains($0) }
  143. }
  144. @MainActor func calculateChartData() {
  145. var basals: [BasalProfile] = []
  146. let tzOffset = TimeZone.current.secondsFromGMT() * -1
  147. basals.append(contentsOf: items.enumerated().map { index, item in
  148. let startDate = Date(timeIntervalSinceReferenceDate: self.timeValues[item.timeIndex])
  149. var endDate = Date(timeIntervalSinceReferenceDate: self.timeValues.last!).addingTimeInterval(30 * 60)
  150. if self.items.count > index + 1 {
  151. let nextItem = self.items[index + 1]
  152. endDate = Date(timeIntervalSinceReferenceDate: self.timeValues[nextItem.timeIndex])
  153. }
  154. return BasalProfile(
  155. amount: Double(self.rateValues[item.rateIndex]),
  156. isOverwritten: false,
  157. startDate: startDate.addingTimeInterval(TimeInterval(tzOffset)),
  158. endDate: endDate.addingTimeInterval(TimeInterval(tzOffset))
  159. )
  160. })
  161. basals.sort(by: {
  162. $0.startDate > $1.startDate
  163. })
  164. chartData = basals
  165. }
  166. }
  167. }