BasalProfileEditorStateModel.swift 6.5 KB

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