OnboardingStateModel.swift 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590
  1. import Combine
  2. import FirebaseCrashlytics
  3. import Foundation
  4. import LoopKit
  5. import Observation
  6. import SwiftUI
  7. /// Model that holds the data collected during onboarding.
  8. extension Onboarding {
  9. @Observable final class StateModel: BaseStateModel<Provider> {
  10. @ObservationIgnored @Injected() var fileStorage: FileStorage!
  11. @ObservationIgnored @Injected() var deviceManager: DeviceDataManager!
  12. @ObservationIgnored @Injected() var broadcaster: Broadcaster!
  13. @ObservationIgnored @Injected() var keychain: Keychain!
  14. @ObservationIgnored @Injected() var nightscoutManager: NightscoutManager!
  15. @ObservationIgnored @Injected() var notificationsManager: UserNotificationsManager!
  16. @ObservationIgnored @Injected() var bluetoothManager: BluetoothStateManager!
  17. private let settingsProvider = PickerSettingsProvider.shared
  18. // MARK: - App Diagnostics
  19. var diagnosticsSharingOption: DiagnosticsSharingOption = .enabled
  20. var hasAcceptedPrivacyPolicy: Bool = false
  21. // MARK: - Important Startup Notes
  22. var hasReadImportantStartupNotes: Bool = false
  23. // MARK: - Nightscout Setup
  24. var nightscoutSetupOption: NightscoutSetupOption = .noSelection
  25. var nightscoutImportOption: NightscoutImportOption = .noSelection
  26. var nightscoutUrl = ""
  27. var nightscoutSecret = ""
  28. var nightscoutResponseMessage = ""
  29. var isValidNightscoutURL: Bool = false
  30. var isConnectingToNS: Bool = false
  31. var isConnectedToNS: Bool = false
  32. var nightscoutImportErrors: [String] = []
  33. var nightscoutImportStatus: ImportStatus = .finished
  34. // MARK: - Units and Pump Omboarding Option
  35. var units: GlucoseUnits = .mgdL
  36. var pumpOptionForOnboardingUnits: PumpOptionForOnboardingUnits = .omnipodDash
  37. // MARK: - Time Values (shared)
  38. let sharedTimeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }.sorted()
  39. // MARK: - Carb Ratio
  40. let carbRatioPickerSetting = PickerSetting(value: 10, step: 0.1, min: 1, max: 50, type: .gram)
  41. var carbRatioItems: [CarbRatioEditor.Item] = []
  42. var initialCarbRatioItems: [CarbRatioEditor.Item] = []
  43. var carbRatioTimeValues: [TimeInterval] { sharedTimeValues }
  44. var carbRatioRateValues: [Decimal] { settingsProvider.generatePickerValues(from: carbRatioPickerSetting, units: units) }
  45. // MARK: - Basal Profile
  46. var basalRatePickerSetting: PickerSetting {
  47. switch pumpOptionForOnboardingUnits {
  48. case .dana:
  49. return PickerSetting(value: 0.05, step: 0.05, min: 0, max: 3, type: .insulinUnitPerHour)
  50. case .minimed:
  51. return PickerSetting(value: 0.05, step: 0.05, min: 0, max: 35, type: .insulinUnitPerHour)
  52. case .omnipodDash:
  53. return PickerSetting(value: 0.05, step: 0.05, min: 0, max: 30, type: .insulinUnitPerHour)
  54. case .omnipodEros:
  55. return PickerSetting(value: 0.05, step: 0.05, min: 0.05, max: 30, type: .insulinUnitPerHour)
  56. }
  57. }
  58. var basalProfileItems: [BasalProfileEditor.Item] = []
  59. var initialBasalProfileItems: [BasalProfileEditor.Item] = []
  60. var basalProfileTimeValues: [TimeInterval] { sharedTimeValues }
  61. var basalProfileRateValues: [Decimal] { settingsProvider.generatePickerValues(from: basalRatePickerSetting, units: units)
  62. }
  63. // MARK: - Insulin Sensitivity Factor (ISF)
  64. var sensitivityPickerSetting = PickerSetting(value: 100, step: 1, min: 9, max: 540, type: .glucose)
  65. var isfItems: [ISFEditor.Item] = []
  66. var initialISFItems: [ISFEditor.Item] = []
  67. var isfTimeValues: [TimeInterval] { sharedTimeValues }
  68. var isfRateValues: [Decimal] { settingsProvider.generatePickerValues(from: sensitivityPickerSetting, units: units) }
  69. // MARK: - Glucose Targets
  70. let letTargetPickerSetting = PickerSetting(value: 100, step: 1, min: 72, max: 180, type: .glucose)
  71. var targetItems: [TargetsEditor.Item] = []
  72. var initialTargetItems: [TargetsEditor.Item] = []
  73. var targetTimeValues: [TimeInterval] { sharedTimeValues }
  74. var targetRateValues: [Decimal] { settingsProvider.generatePickerValues(from: letTargetPickerSetting, units: units) }
  75. // MARK: - Delivery Limit Defaults
  76. var maxBolus: Decimal = 10
  77. var maxBasal: Decimal = 2
  78. var maxIOB: Decimal = 0
  79. var maxCOB: Decimal = 120
  80. var minimumSafetyThreshold: Decimal = 60
  81. // MARK: - Algorithm Settings Defaults & State
  82. var hasReadAlgorithmSetupInformation: Bool = false
  83. var autosensMin: Decimal = 0.7
  84. var autosensMax: Decimal = 1.2
  85. var rewindResetsAutosens: Bool = true
  86. var enableSMBAlways: Bool = false
  87. var enableSMBWithCOB: Bool = false
  88. var enableSMBWithTempTarget: Bool = false
  89. var enableSMBAfterCarbs: Bool = false
  90. var enableSMBWithHighGlucoseTarget: Bool = false
  91. var highGlucoseTarget: Decimal = 110
  92. var allowSMBWithHighTempTarget: Bool = false
  93. var enableUAM: Bool = false
  94. var maxSMBMinutes: Decimal = 30
  95. var maxUAMMinutes: Decimal = 30
  96. var maxDeltaGlucoseThreshold: Decimal = 0.2
  97. var highTempTargetRaisesSensitivity: Bool = false
  98. var lowTempTargetLowersSensitivity: Bool = false
  99. var sensitivityRaisesTarget: Bool = false
  100. var resistanceLowersTarget: Bool = false
  101. var halfBasalTarget: Decimal = 160
  102. // MARK: - Permission Requests
  103. var hasNotificationsGranted = false
  104. var shouldDisplayBluetoothRequestAlert: Bool = false
  105. var hasBluetoothGranted = false
  106. // MARK: - Subscribe
  107. override func subscribe() {
  108. // Keychain items are not removed, even after uninstalling the app. Attempt to read them initially.
  109. nightscoutUrl = keychain.getValue(String.self, forKey: NightscoutConfig.Config.urlKey) ?? ""
  110. nightscoutSecret = keychain.getValue(String.self, forKey: NightscoutConfig.Config.secretKey) ?? ""
  111. isConnectedToNS = false
  112. isConnectingToNS = false
  113. isValidNightscoutURL = false
  114. // Attempt to fetch existing units, therapy settings and delivery limits from file
  115. units = settingsManager.settings.units
  116. fetchExistingTherapySettingsFromFile()
  117. fetchExistingDeliveryLimtisFromFile()
  118. }
  119. // MARK: - Helpers
  120. /// Finds the index of the closest `Decimal` value in the given array.
  121. /// - Parameters:
  122. /// - value: The value to match.
  123. /// - array: The array to search in.
  124. /// - Returns: Closest index in array.
  125. func closestIndex(for value: Decimal, in array: [Decimal]) -> Int {
  126. array.enumerated().min(by: {
  127. abs($0.element - value) < abs($1.element - value)
  128. })?.offset ?? 0
  129. }
  130. /// Finds the index of the closest `TimeInterval` value in the given array.
  131. /// - Parameters:
  132. /// - value: The time value to match.
  133. /// - array: The array to search in.
  134. /// - Returns: Closest index in array.
  135. func closestIndex(for value: TimeInterval, in array: [TimeInterval]) -> Int {
  136. array.enumerated().min(by: {
  137. abs($0.element - value) < abs($1.element - value)
  138. })?.offset ?? 0
  139. }
  140. /// A date formatter for time strings used in saved settings.
  141. private var timeFormatter: DateFormatter {
  142. let formatter = DateFormatter()
  143. formatter.timeZone = TimeZone(secondsFromGMT: 0)
  144. formatter.dateFormat = "HH:mm:ss"
  145. return formatter
  146. }
  147. // MARK: - Fetch existing therapy settings from file
  148. /// Loads existing therapy settings from the provider and maps them into UI editor items.
  149. ///
  150. /// This function processes therapy-related configurations (glucose targets, basal rates,
  151. /// carb ratios, and insulin sensitivity factors) stored in file-backed models from the provider.
  152. /// It calculates the closest matching indices for time and rate values to map them to corresponding
  153. /// `Editor.Item` models for use in the UI.
  154. ///
  155. /// - Populates:
  156. /// - `targetItems` and `initialTargetItems` with glucose target entries.
  157. /// - `basalProfileItems` and `initialBasalProfileItems` with basal rate entries.
  158. /// - `carbRatioItems` and `initialCarbRatioItems` with carbohydrate ratio entries.
  159. /// - `isfItems` and `initialISFItems` with insulin sensitivity factor entries.
  160. func fetchExistingTherapySettingsFromFile() {
  161. targetItems = provider.glucoseTargetsOnFile.targets.map { value in
  162. let timeIndex = closestIndex(for: TimeInterval(Double(value.offset * 60)), in: targetTimeValues)
  163. let lowIndex = closestIndex(for: value.low, in: targetRateValues)
  164. let highIndex = closestIndex(for: value.high, in: targetRateValues)
  165. return TargetsEditor.Item(lowIndex: lowIndex, highIndex: highIndex, timeIndex: timeIndex)
  166. }
  167. initialTargetItems = targetItems
  168. .map { TargetsEditor.Item(lowIndex: $0.lowIndex, highIndex: $0.highIndex, timeIndex: $0.timeIndex) }
  169. basalProfileItems = provider.basalProfileOnFile.map { value in
  170. let timeIndex = closestIndex(for: TimeInterval(Double(value.minutes * 60)), in: basalProfileTimeValues)
  171. let rateIndex = closestIndex(for: value.rate, in: basalProfileRateValues)
  172. return BasalProfileEditor.Item(rateIndex: rateIndex, timeIndex: timeIndex)
  173. }
  174. initialBasalProfileItems = basalProfileItems
  175. .map { BasalProfileEditor.Item(rateIndex: $0.rateIndex, timeIndex: $0.timeIndex) }
  176. carbRatioItems = provider.carbRatiosOnFile.schedule.map { value in
  177. let timeIndex = closestIndex(for: TimeInterval(Double(value.offset * 60)), in: carbRatioTimeValues)
  178. let rateIndex = closestIndex(for: value.ratio, in: carbRatioRateValues)
  179. return CarbRatioEditor.Item(rateIndex: rateIndex, timeIndex: timeIndex)
  180. }
  181. initialCarbRatioItems = carbRatioItems.map { CarbRatioEditor.Item(rateIndex: $0.rateIndex, timeIndex: $0.timeIndex) }
  182. isfItems = provider.isfOnFile.sensitivities.map { value in
  183. let timeIndex = closestIndex(for: TimeInterval(Double(value.offset * 60)), in: isfTimeValues)
  184. let rateIndex = closestIndex(for: value.sensitivity, in: isfRateValues)
  185. return ISFEditor.Item(rateIndex: rateIndex, timeIndex: timeIndex)
  186. }
  187. initialISFItems = isfItems.map { ISFEditor.Item(rateIndex: $0.rateIndex, timeIndex: $0.timeIndex) }
  188. }
  189. /// Loads delivery limit settings (Units, Max IOB, Max COB, Max Bolus, Max Basal) from the provider.
  190. ///
  191. /// Retrieves pump-related safety and delivery limits from both the provider's
  192. /// file-backed pump settings and app-specific preferences. These values are used
  193. /// to pre-fill the delivery limits editor in the onboarding or settings UI.
  194. ///
  195. /// - Populates:
  196. /// - `maxBolus` and `maxBasal` from file-based pump settings.
  197. /// - `maxIOB`, `maxCOB`, and `minimumSafetyThreshold` from app preferences.
  198. /// - `units` from app settings.
  199. func fetchExistingDeliveryLimtisFromFile() {
  200. let pumpSettingsFromFile = provider.pumpSettingsFromFile
  201. if let pumpSettingsFromFile = pumpSettingsFromFile {
  202. maxBolus = pumpSettingsFromFile.maxBolus
  203. maxBasal = pumpSettingsFromFile.maxBasal
  204. }
  205. let preferences = settingsManager.preferences
  206. maxIOB = preferences.maxIOB
  207. maxCOB = preferences.maxCOB
  208. minimumSafetyThreshold = preferences.threshold_setting
  209. }
  210. // MARK: - Get Therapy Items
  211. /// Converts ISF editor items to a list of `TherapySettingItem`.
  212. /// - Returns: Sorted list of therapy setting items based on ISF.
  213. func getISFTherapyItems() -> [TherapySettingItem] {
  214. getTherapyItems(from: isfItems, rateValues: isfRateValues, timeValues: isfTimeValues)
  215. }
  216. /// Converts basal profile editor items to a list of `TherapySettingItem`.
  217. /// - Returns: Sorted list of therapy setting items based on basal rates.
  218. func getBasalTherapyItems() -> [TherapySettingItem] {
  219. getTherapyItems(
  220. from: basalProfileItems,
  221. rateValues: basalProfileRateValues,
  222. timeValues: basalProfileTimeValues
  223. )
  224. }
  225. /// Converts carb ratio editor items to a list of `TherapySettingItem`.
  226. /// - Returns: Sorted list of therapy setting items based on carb ratios.
  227. func getCarbRatioTherapyItems() -> [TherapySettingItem] {
  228. getTherapyItems(from: carbRatioItems, rateValues: carbRatioRateValues, timeValues: carbRatioTimeValues)
  229. }
  230. /// Converts glucose target editor items to a list of `TherapySettingItem`.
  231. /// - Returns: Sorted list of therapy setting items based on glucose targets.
  232. func getTargetTherapyItems() -> [TherapySettingItem] {
  233. targetItems.map {
  234. TherapySettingItem(
  235. time: targetTimeValues[$0.timeIndex],
  236. value: targetRateValues[$0.lowIndex]
  237. )
  238. }.sorted { $0.time < $1.time }
  239. }
  240. /// Generic helper to convert any type of editor item into therapy setting items.
  241. /// - Parameters:
  242. /// - items: An array of items conforming to `TherapyItemConvertible`.
  243. /// - rateValues: The rate values to be used.
  244. /// - timeValues: The time values to be used.
  245. /// - Returns: A sorted array of `TherapySettingItem`.
  246. private func getTherapyItems<T: TherapyItemConvertible>(
  247. from items: [T],
  248. rateValues: [Decimal],
  249. timeValues: [TimeInterval]
  250. ) -> [TherapySettingItem] {
  251. items.map {
  252. TherapySettingItem(
  253. time: timeValues[$0.timeIndex],
  254. value: rateValues[$0.rateIndex]
  255. )
  256. }.sorted { $0.time < $1.time }
  257. }
  258. // MARK: - Unified Update Methods
  259. /// Updates the ISF editor items based on the provided therapy setting items.
  260. /// - Parameter therapyItems: The list of therapy items to update from.
  261. func updateISF(from therapyItems: [TherapySettingItem]) {
  262. isfItems = therapyItems.map {
  263. ISFEditor.Item(
  264. rateIndex: closestIndex(for: $0.value, in: isfRateValues),
  265. timeIndex: closestIndex(for: $0.time, in: isfTimeValues)
  266. )
  267. }.sorted { $0.timeIndex < $1.timeIndex }
  268. }
  269. /// Updates the basal rate editor items based on the provided therapy setting items.
  270. /// - Parameter therapyItems: The list of therapy items to update from.
  271. func updateBasal(from therapyItems: [TherapySettingItem]) {
  272. basalProfileItems = therapyItems.map {
  273. BasalProfileEditor.Item(
  274. rateIndex: closestIndex(for: $0.value, in: basalProfileRateValues),
  275. timeIndex: closestIndex(for: $0.time, in: basalProfileTimeValues)
  276. )
  277. }.sorted { $0.timeIndex < $1.timeIndex }
  278. }
  279. /// Updates the carb ratio editor items based on the provided therapy setting items.
  280. /// - Parameter therapyItems: The list of therapy items to update from.
  281. func updateCarbRatio(from therapyItems: [TherapySettingItem]) {
  282. carbRatioItems = therapyItems.map {
  283. CarbRatioEditor.Item(
  284. rateIndex: closestIndex(for: $0.value, in: carbRatioRateValues),
  285. timeIndex: closestIndex(for: $0.time, in: carbRatioTimeValues)
  286. )
  287. }.sorted { $0.timeIndex < $1.timeIndex }
  288. }
  289. /// Updates the glucose target editor items based on the provided therapy setting items.
  290. /// - Parameter therapyItems: The list of therapy items to update from.
  291. func updateTargets(from therapyItems: [TherapySettingItem]) {
  292. targetItems = therapyItems.map {
  293. let rateIndex = closestIndex(for: $0.value, in: targetRateValues)
  294. let timeIndex = closestIndex(for: $0.time, in: targetTimeValues)
  295. return TargetsEditor.Item(
  296. lowIndex: rateIndex,
  297. highIndex: rateIndex,
  298. timeIndex: timeIndex
  299. )
  300. }.sorted { $0.timeIndex < $1.timeIndex }
  301. }
  302. // MARK: - Add Initials
  303. /// Adds a default ISF editor item at 00:00 with a standard sensitivity value.
  304. func addInitialISF() {
  305. addInitialItem(
  306. defaultValue: 50,
  307. rateValues: isfRateValues,
  308. assign: { isfItems = $0 },
  309. makeItem: ISFEditor.Item.init
  310. )
  311. }
  312. /// Adds a default basal rate editor item at 00:00 with a typical rate value.
  313. func addInitialBasalRate() {
  314. addInitialItem(
  315. defaultValue: 0.1,
  316. rateValues: basalProfileRateValues,
  317. assign: { basalProfileItems = $0 },
  318. makeItem: BasalProfileEditor.Item.init
  319. )
  320. }
  321. /// Adds a default carb ratio editor item at 00:00 with a standard ratio.
  322. func addInitialCarbRatio() {
  323. addInitialItem(
  324. defaultValue: 10,
  325. rateValues: carbRatioRateValues,
  326. assign: { carbRatioItems = $0 },
  327. makeItem: CarbRatioEditor.Item.init
  328. )
  329. }
  330. /// Adds a default glucose target item at 00:00 with a typical target value.
  331. func addInitialTarget() {
  332. let timeIndex = 0
  333. let rateIndex = closestIndex(for: 100, in: targetRateValues)
  334. targetItems = [TargetsEditor.Item(lowIndex: rateIndex, highIndex: rateIndex, timeIndex: timeIndex)]
  335. }
  336. /// Adds an initial therapy setting item for a given editor item type.
  337. /// - Parameters:
  338. /// - defaultValue: The expected default value to use.
  339. /// - rateValues: The array of rate values for the item.
  340. /// - assign: A closure that assigns the newly created array to the correct property.
  341. private func addInitialItem<ItemType>(
  342. defaultValue: Decimal,
  343. rateValues: [Decimal],
  344. assign: ([ItemType]) -> Void,
  345. makeItem: (Int, Int) -> ItemType
  346. ) {
  347. let timeIndex = 0
  348. let rateIndex = closestIndex(for: defaultValue, in: rateValues)
  349. assign([makeItem(rateIndex, timeIndex)])
  350. }
  351. // MARK: - Validate
  352. /// Removes duplicate entries from `carbRatioItems`, ensures sorting by time index,
  353. /// and forces the first entry to start at 00:00 (timeIndex 0).
  354. func validateCarbRatios() {
  355. carbRatioItems = validated(items: carbRatioItems, timeIndexKeyPath: \.timeIndex)
  356. }
  357. /// Removes duplicate entries from `basalProfileItems`, ensures sorting by time index,
  358. /// and forces the first entry to start at 00:00 (timeIndex 0).
  359. func validateBasal() {
  360. basalProfileItems = validated(items: basalProfileItems, timeIndexKeyPath: \.timeIndex)
  361. }
  362. /// Removes duplicate entries from `isfItems`, ensures sorting by time index,
  363. /// and forces the first entry to start at 00:00 (timeIndex 0).
  364. func validateISF() {
  365. isfItems = validated(items: isfItems, timeIndexKeyPath: \.timeIndex)
  366. }
  367. /// Removes duplicate entries from `targetItems`, ensures sorting by time index,
  368. /// and forces the first entry to start at 00:00 (timeIndex 0).
  369. func validateTarget() {
  370. targetItems = validated(items: targetItems, timeIndexKeyPath: \.timeIndex)
  371. }
  372. /// Removes duplicates, sorts by time, and ensures the first entry starts at 00:00.
  373. /// - Parameters:
  374. /// - items: The list of items to validate.
  375. /// - timeIndexKeyPath: A writable key path to the timeIndex property.
  376. /// - Returns: A validated and sorted list of items with the first entry at 00:00.
  377. private func validated<T: Hashable>(items: [T], timeIndexKeyPath: WritableKeyPath<T, Int>) -> [T] {
  378. var result = Array(Set(items)).sorted { $0[keyPath: timeIndexKeyPath] < $1[keyPath: timeIndexKeyPath] }
  379. if !result.isEmpty, result[0][keyPath: timeIndexKeyPath] != 0 {
  380. result[0][keyPath: timeIndexKeyPath] = 0
  381. }
  382. return result
  383. }
  384. // MARK: - Save
  385. /// Saves the carb ratio items to file storage and sets them as initial values.
  386. func saveCarbRatios() {
  387. let schedule = carbRatioItems.map { item in
  388. let time = timeFormatter.string(from: Date(timeIntervalSince1970: carbRatioTimeValues[item.timeIndex]))
  389. let offset = Int(carbRatioTimeValues[item.timeIndex] / 60)
  390. let value = carbRatioRateValues[item.rateIndex]
  391. return CarbRatioEntry(start: time, offset: offset, ratio: value)
  392. }
  393. fileStorage.save(CarbRatios(units: .grams, schedule: schedule), as: OpenAPS.Settings.carbRatios)
  394. initialCarbRatioItems = carbRatioItems
  395. }
  396. /// Saves the basal profile items to file storage and sets them as initial values.
  397. func saveBasalProfile() {
  398. let profile = basalProfileItems.map { item in
  399. let time = timeFormatter.string(from: Date(timeIntervalSince1970: basalProfileTimeValues[item.timeIndex]))
  400. let offset = Int(basalProfileTimeValues[item.timeIndex] / 60)
  401. let rate = basalProfileRateValues[item.rateIndex]
  402. return BasalProfileEntry(start: time, minutes: offset, rate: rate)
  403. }
  404. fileStorage.save(profile, as: OpenAPS.Settings.basalProfile)
  405. initialBasalProfileItems = basalProfileItems
  406. }
  407. /// Saves the insulin sensitivity (ISF) items to file storage and sets them as initial values.
  408. func saveISFValues() {
  409. let sensitivities = isfItems.map { item in
  410. let time = timeFormatter.string(from: Date(timeIntervalSince1970: isfTimeValues[item.timeIndex]))
  411. let offset = Int(isfTimeValues[item.timeIndex] / 60)
  412. let value = isfRateValues[item.rateIndex]
  413. return InsulinSensitivityEntry(sensitivity: value, offset: offset, start: time)
  414. }
  415. let profile = InsulinSensitivities(units: .mgdL, userPreferredUnits: .mgdL, sensitivities: sensitivities)
  416. fileStorage.save(profile, as: OpenAPS.Settings.insulinSensitivities)
  417. initialISFItems = isfItems
  418. }
  419. /// Saves the glucose target items to file storage and sets them as initial values.
  420. func saveTargets() {
  421. let targets = targetItems.map { item in
  422. let time = timeFormatter.string(from: Date(timeIntervalSince1970: targetTimeValues[item.timeIndex]))
  423. let offset = Int(targetTimeValues[item.timeIndex] / 60)
  424. let value = targetRateValues[item.lowIndex]
  425. return BGTargetEntry(low: value, high: value, start: time, offset: offset)
  426. }
  427. let profile = BGTargets(units: .mgdL, userPreferredUnits: .mgdL, targets: targets)
  428. fileStorage.save(profile, as: OpenAPS.Settings.bgTargets)
  429. initialTargetItems = targetItems
  430. }
  431. /// Persists all onboarding data by applying settings and saving therapy values.
  432. func saveOnboardingData() {
  433. applyDiagnostics()
  434. applyToSettings()
  435. applyToPreferences()
  436. applyToPumpSettings()
  437. saveTargets()
  438. saveBasalProfile()
  439. saveCarbRatios()
  440. saveISFValues()
  441. }
  442. /// Persists the current diagnostics sharing option to UserDefaults as a boolean.
  443. func applyDiagnostics() {
  444. let booleanValue: Bool = diagnosticsSharingOption == .enabled
  445. UserDefaults.standard.set(booleanValue, forKey: "DiagnosticsSharing")
  446. Crashlytics.crashlytics().setCrashlyticsCollectionEnabled(booleanValue)
  447. }
  448. /// Applies the selected glucose units to the app's settings.
  449. func applyToSettings() {
  450. var settingsCopy = settingsManager.settings
  451. settingsCopy.units = units
  452. settingsManager.settings = settingsCopy
  453. }
  454. /// Applies the selected delivery preferences to the app's settings.
  455. func applyToPreferences() {
  456. var preferences = Preferences()
  457. // delivery limits (those that are preference-bound, not pump-settings-bound
  458. preferences.maxIOB = maxIOB
  459. preferences.maxCOB = maxCOB
  460. preferences.threshold_setting = minimumSafetyThreshold
  461. // autosens
  462. preferences.autosensMin = autosensMin
  463. preferences.autosensMax = autosensMax
  464. preferences.rewindResetsAutosens = rewindResetsAutosens
  465. // smb settings
  466. preferences.enableSMBAlways = enableSMBAlways
  467. preferences.enableSMBWithCOB = enableSMBWithCOB
  468. preferences.enableSMBWithTemptarget = enableSMBWithTempTarget
  469. preferences.enableSMBAfterCarbs = enableSMBAfterCarbs
  470. preferences.enableSMB_high_bg = enableSMBWithHighGlucoseTarget
  471. preferences.enableSMB_high_bg_target = highGlucoseTarget
  472. preferences.allowSMBWithHighTemptarget = allowSMBWithHighTempTarget
  473. preferences.enableUAM = enableUAM
  474. preferences.maxSMBBasalMinutes = maxSMBMinutes
  475. preferences.maxUAMSMBBasalMinutes = maxUAMMinutes
  476. preferences.maxDeltaBGthreshold = maxDeltaGlucoseThreshold
  477. // target behavior
  478. preferences.highTemptargetRaisesSensitivity = highTempTargetRaisesSensitivity
  479. preferences.lowTemptargetLowersSensitivity = lowTempTargetLowersSensitivity
  480. preferences.sensitivityRaisesTarget = sensitivityRaisesTarget
  481. preferences.resistanceLowersTarget = resistanceLowersTarget
  482. preferences.halfBasalExerciseTarget = halfBasalTarget
  483. settingsManager.preferences = preferences
  484. }
  485. /// Saves pump delivery limits to persistent storage and broadcasts changes.
  486. func applyToPumpSettings() {
  487. let defaultDIA = settingsProvider.settings.dia.value
  488. let pumpSettings = PumpSettings(insulinActionCurve: defaultDIA, maxBolus: maxBolus, maxBasal: maxBasal)
  489. fileStorage.save(pumpSettings, as: OpenAPS.Settings.settings)
  490. }
  491. }
  492. }
  493. // MARK: - Protocol (optional) to unify type mapping
  494. protocol TherapyItemConvertible {
  495. var rateIndex: Int { get }
  496. var timeIndex: Int { get }
  497. }
  498. extension ISFEditor.Item: TherapyItemConvertible {}
  499. extension CarbRatioEditor.Item: TherapyItemConvertible {}
  500. extension BasalProfileEditor.Item: TherapyItemConvertible {}