ProfileManager.swift 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. // LoopFollow
  2. // ProfileManager.swift
  3. // Created by Jonas Björkert on 2024-07-15.
  4. import Foundation
  5. import HealthKit
  6. final class ProfileManager {
  7. // MARK: - Singleton Instance
  8. static let shared = ProfileManager()
  9. // MARK: - Properties
  10. var isfSchedule: [TimeValue<HKQuantity>]
  11. var basalSchedule: [TimeValue<Double>]
  12. var carbRatioSchedule: [TimeValue<Double>]
  13. var targetLowSchedule: [TimeValue<HKQuantity>]
  14. var targetHighSchedule: [TimeValue<HKQuantity>]
  15. var loopOverrides: [LoopOverride]
  16. var trioOverrides: [TrioOverride]
  17. var units: HKUnit
  18. var timezone: TimeZone
  19. var defaultProfile: String
  20. // MARK: - Nested Structures
  21. struct TimeValue<T> {
  22. let timeAsSeconds: Int
  23. let value: T
  24. }
  25. struct LoopOverride {
  26. let name: String
  27. let targetRange: [HKQuantity]
  28. let duration: Int?
  29. let insulinNeedsScaleFactor: Double
  30. let symbol: String
  31. }
  32. struct TrioOverride {
  33. let name: String
  34. let duration: Double?
  35. let percentage: Double?
  36. let target: HKQuantity?
  37. }
  38. // MARK: - Initializer
  39. private init() {
  40. isfSchedule = []
  41. basalSchedule = []
  42. carbRatioSchedule = []
  43. targetLowSchedule = []
  44. targetHighSchedule = []
  45. loopOverrides = []
  46. trioOverrides = []
  47. units = .millimolesPerLiter
  48. timezone = TimeZone.current
  49. defaultProfile = ""
  50. }
  51. // MARK: - Methods
  52. func loadProfile(from profileData: NSProfile) {
  53. guard let store = profileData.store[profileData.defaultProfile] else {
  54. return
  55. }
  56. units = store.units.lowercased() == "mg/dl" ? .milligramsPerDeciliter : .millimolesPerLiter
  57. defaultProfile = profileData.defaultProfile
  58. timezone = getTimeZone(from: store.timezone)
  59. isfSchedule = store.sens.map { TimeValue(timeAsSeconds: Int($0.timeAsSeconds), value: HKQuantity(unit: self.units, doubleValue: $0.value)) }
  60. basalSchedule = store.basal.map { TimeValue(timeAsSeconds: Int($0.timeAsSeconds), value: $0.value) }
  61. carbRatioSchedule = store.carbratio.map { TimeValue(timeAsSeconds: Int($0.timeAsSeconds), value: $0.value) }
  62. targetLowSchedule = store.target_low?.map { TimeValue(timeAsSeconds: Int($0.timeAsSeconds), value: HKQuantity(unit: self.units, doubleValue: $0.value)) } ?? []
  63. targetHighSchedule = store.target_high?.map { TimeValue(timeAsSeconds: Int($0.timeAsSeconds), value: HKQuantity(unit: self.units, doubleValue: $0.value)) } ?? []
  64. if let loopSettings = profileData.loopSettings,
  65. let overridePresets = loopSettings.overridePresets
  66. {
  67. loopOverrides = overridePresets.map { preset in
  68. let targetRangeQuantities = preset.targetRange?.map { HKQuantity(unit: self.units, doubleValue: $0) }
  69. return LoopOverride(
  70. name: preset.name,
  71. targetRange: targetRangeQuantities ?? [],
  72. duration: preset.duration,
  73. insulinNeedsScaleFactor: preset.insulinNeedsScaleFactor ?? 1.0,
  74. symbol: preset.symbol ?? ""
  75. )
  76. }
  77. } else {
  78. loopOverrides = []
  79. }
  80. if let trioOverrides = profileData.trioOverrides {
  81. self.trioOverrides = trioOverrides.map { entry in
  82. let targetQuantity = entry.target != nil ? HKQuantity(unit: .milligramsPerDeciliter, doubleValue: entry.target!) : nil
  83. return TrioOverride(
  84. name: entry.name,
  85. duration: entry.duration,
  86. percentage: entry.percentage,
  87. target: targetQuantity
  88. )
  89. }
  90. } else {
  91. trioOverrides = []
  92. }
  93. Storage.shared.deviceToken.value = profileData.deviceToken ?? ""
  94. if let expirationDate = profileData.expirationDate {
  95. Storage.shared.expirationDate.value = NightscoutUtils.parseDate(expirationDate)
  96. } else {
  97. Storage.shared.expirationDate.value = nil
  98. }
  99. Storage.shared.bundleId.value = profileData.bundleIdentifier ?? ""
  100. Storage.shared.productionEnvironment.value = profileData.isAPNSProduction ?? false
  101. Storage.shared.teamId.value = profileData.teamID ?? Storage.shared.teamId.value ?? ""
  102. }
  103. func currentISF() -> HKQuantity? {
  104. return getCurrentValue(from: isfSchedule)
  105. }
  106. func currentBasal() -> String? {
  107. if let basal = getCurrentValue(from: basalSchedule) {
  108. return Localizer.formatToLocalizedString(basal, maxFractionDigits: 2, minFractionDigits: 0)
  109. }
  110. return nil
  111. }
  112. func currentCarbRatio() -> Double? {
  113. return getCurrentValue(from: carbRatioSchedule)
  114. }
  115. func currentTargetLow() -> HKQuantity? {
  116. return getCurrentValue(from: targetLowSchedule)
  117. }
  118. func currentTargetHigh() -> HKQuantity? {
  119. return getCurrentValue(from: targetHighSchedule)
  120. }
  121. private func getCurrentValue<T>(from schedule: [TimeValue<T>]) -> T? {
  122. guard !schedule.isEmpty else { return nil }
  123. let now = Date()
  124. var calendar = Calendar.current
  125. calendar.timeZone = timezone
  126. let currentTimeInSeconds = calendar.component(.hour, from: now) * 3600 +
  127. calendar.component(.minute, from: now) * 60 +
  128. calendar.component(.second, from: now)
  129. var lastValue: T?
  130. for timeValue in schedule {
  131. if currentTimeInSeconds >= timeValue.timeAsSeconds {
  132. lastValue = timeValue.value
  133. } else {
  134. break
  135. }
  136. }
  137. return lastValue
  138. }
  139. private func getTimeZone(from identifier: String) -> TimeZone {
  140. if let timeZone = TimeZone(identifier: identifier) {
  141. return timeZone
  142. }
  143. let adjustedIdentifier = identifier.replacingOccurrences(of: "ETC/", with: "Etc/")
  144. if let timeZone = TimeZone(identifier: adjustedIdentifier) {
  145. return timeZone
  146. }
  147. if identifier.uppercased().contains("GMT") {
  148. let components = identifier.uppercased().components(separatedBy: "GMT")
  149. if components.count > 1 {
  150. let offsetString = components[1]
  151. if let offsetHours = Int(offsetString) {
  152. let correctedOffsetHours = -offsetHours
  153. let secondsFromGMT = correctedOffsetHours * 3600
  154. if let timeZone = TimeZone(secondsFromGMT: secondsFromGMT) {
  155. return timeZone
  156. }
  157. }
  158. }
  159. }
  160. return TimeZone.current
  161. }
  162. func clear() {
  163. isfSchedule = []
  164. basalSchedule = []
  165. carbRatioSchedule = []
  166. targetLowSchedule = []
  167. targetHighSchedule = []
  168. loopOverrides = []
  169. trioOverrides = []
  170. units = .millimolesPerLiter
  171. timezone = TimeZone.current
  172. defaultProfile = ""
  173. }
  174. }