ProfileManager.swift 7.0 KB

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