OnboardingStateModel.swift 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680
  1. import Combine
  2. import Foundation
  3. import LoopKit
  4. import Observation
  5. import SwiftUI
  6. /// Represents the different steps in the onboarding process.
  7. enum OnboardingStep: Int, CaseIterable, Identifiable {
  8. case welcome
  9. case unitSelection
  10. case glucoseTarget
  11. case basalProfile
  12. case carbRatio
  13. case insulinSensitivity
  14. case deliveryLimits
  15. case completed
  16. var id: Int { rawValue }
  17. var hasSubsteps: Bool {
  18. self == .deliveryLimits
  19. }
  20. var substeps: [DeliveryLimitSubstep] {
  21. guard hasSubsteps else { return [] }
  22. return DeliveryLimitSubstep.allCases
  23. }
  24. /// The title to display for this onboarding step.
  25. var title: String {
  26. switch self {
  27. case .welcome:
  28. return "Welcome to Trio"
  29. case .unitSelection:
  30. return "Units & Pump"
  31. case .glucoseTarget:
  32. return "Glucose Target"
  33. case .basalProfile:
  34. return "Basal Profile"
  35. case .carbRatio:
  36. return "Carbohydrate Ratio"
  37. case .insulinSensitivity:
  38. return "Insulin Sensitivity"
  39. case .deliveryLimits:
  40. return "Delivery Limits"
  41. case .completed:
  42. return "All Set!"
  43. }
  44. }
  45. /// A detailed description of what this onboarding step is about.
  46. var description: String {
  47. switch self {
  48. case .welcome:
  49. return "Trio is a powerful app that helps you manage your diabetes. Let's get started by setting up a few important parameters that will help Trio work effectively for you."
  50. case .unitSelection:
  51. return "Before you can begin with configuring your therapy settigns, Trio needs to know which units you use for your glucose and insulin measurements (based on your pump model)."
  52. case .glucoseTarget:
  53. return "Your glucose target is the blood glucose level you aim to maintain. Trio will use this to calculate insulin doses and provide recommendations."
  54. case .basalProfile:
  55. return "Your basal profile represents the amount of background insulin you need throughout the day. This helps Trio calculate your insulin needs."
  56. case .carbRatio:
  57. return "Your carb ratio tells how many grams of carbohydrates one unit of insulin will cover. This is essential for accurate meal bolus calculations."
  58. case .insulinSensitivity:
  59. return "Your insulin sensitivity factor (ISF) indicates how much one unit of insulin will lower your blood glucose. This helps calculate correction boluses."
  60. case .deliveryLimits:
  61. return "Trio offers various delivery limits which represent the maximum amount of insulin it can deliver at a time. This helps ensure safe and effective experience."
  62. case .completed:
  63. return "Great job! You've completed the initial setup of Trio. You can always adjust these settings later in the app."
  64. }
  65. }
  66. /// The system icon name associated with this step.
  67. var iconName: String {
  68. switch self {
  69. case .welcome:
  70. return "hand.wave.fill"
  71. case .unitSelection:
  72. return "numbers.rectangle"
  73. case .glucoseTarget:
  74. return "target"
  75. case .basalProfile:
  76. return "chart.xyaxis.line"
  77. case .carbRatio:
  78. return "fork.knife"
  79. case .insulinSensitivity:
  80. return "drop.fill"
  81. case .deliveryLimits:
  82. return "slider.horizontal.3"
  83. case .completed:
  84. return "checkmark.circle.fill"
  85. }
  86. }
  87. /// Returns the next step in the onboarding process, or nil if this is the last step.
  88. var next: OnboardingStep? {
  89. let allCases = OnboardingStep.allCases
  90. let currentIndex = allCases.firstIndex(of: self) ?? 0
  91. let nextIndex = currentIndex + 1
  92. return nextIndex < allCases.count ? allCases[nextIndex] : nil
  93. }
  94. /// Returns the previous step in the onboarding process, or nil if this is the first step.
  95. var previous: OnboardingStep? {
  96. let allCases = OnboardingStep.allCases
  97. let currentIndex = allCases.firstIndex(of: self) ?? 0
  98. let previousIndex = currentIndex - 1
  99. return previousIndex >= 0 ? allCases[previousIndex] : nil
  100. }
  101. /// The accent color to use for this step.
  102. var accentColor: Color {
  103. switch self {
  104. case .completed,
  105. .deliveryLimits,
  106. .unitSelection,
  107. .welcome:
  108. return Color.blue
  109. case .glucoseTarget:
  110. return Color.green
  111. case .basalProfile:
  112. return Color.purple
  113. case .carbRatio:
  114. return Color.orange
  115. case .insulinSensitivity:
  116. return Color.red
  117. }
  118. }
  119. }
  120. enum DeliveryLimitSubstep: Int, CaseIterable, Identifiable {
  121. case maxIOB
  122. case maxBolus
  123. case maxBasal
  124. case maxCOB
  125. var id: Int { rawValue }
  126. var title: String {
  127. switch self {
  128. case .maxIOB: return String(localized: "Max IOB", comment: "Max IOB")
  129. case .maxBolus: return String(localized: "Max Bolus")
  130. case .maxBasal: return String(localized: "Max Basal")
  131. case .maxCOB: return String(localized: "Max COB", comment: "Max COB")
  132. }
  133. }
  134. var hint: String {
  135. switch self {
  136. case .maxIOB: return String(localized: "Maximum units of insulin allowed to be active.")
  137. case .maxBolus: return String(localized: "Largest bolus of insulin allowed.")
  138. case .maxBasal: return String(localized: "Largest basal rate allowed.")
  139. case .maxCOB: return String(localized: "Maximum Carbs On Board (COB) allowed.")
  140. }
  141. }
  142. var description: any View {
  143. switch self {
  144. case .maxIOB:
  145. return VStack(alignment: .leading, spacing: 8) {
  146. Text(
  147. "This is the maximum amount of Insulin On Board (IOB) above profile basal rates from all sources - positive temporary basal rates, manual or meal boluses, and SMBs - that Trio is allowed to accumulate to address an above target glucose."
  148. )
  149. Text(
  150. "If a calculated amount exceeds this limit, the suggested and / or delivered amount will be reduced so that active insulin on board (IOB) will not exceed this safety limit."
  151. )
  152. Text(
  153. "Note: You can still manually bolus above this limit, but the suggested bolus amount will never exceed this in the bolus calculator."
  154. )
  155. }
  156. case .maxBolus:
  157. return VStack(alignment: .leading, spacing: 8) {
  158. Text(
  159. "This is the maximum bolus allowed to be delivered at one time. This limits manual and automatic bolus."
  160. )
  161. Text("Most set this to their largest meal bolus. Then, adjust if needed.")
  162. Text("If you attempt to request a bolus larger than this, the bolus will not be accepted.")
  163. }
  164. case .maxBasal:
  165. return VStack(alignment: .leading, spacing: 8) {
  166. Text(
  167. "This is the maximum basal rate allowed to be set or scheduled. This applies to both automatic and manual basal rates."
  168. )
  169. Text(
  170. "Note to Medtronic Pump Users: You must also manually set the max basal rate on the pump to this value or higher."
  171. )
  172. }
  173. case .maxCOB:
  174. return VStack(alignment: .leading, spacing: 8) {
  175. Text(
  176. "This setting defines the maximum amount of Carbs On Board (COB) at any given time for Trio to use in dosing calculations. If more carbs are entered than allowed by this limit, Trio will cap the current COB in calculations to Max COB and remain at max until all remaining carbs have shown to be absorbed."
  177. )
  178. Text(
  179. "For example, if Max COB is 120 g and you enter a meal containing 150 g of carbs, your COB will remain at 120 g until the remaining 30 g have been absorbed."
  180. )
  181. Text("This is an important limit when UAM is ON.")
  182. }
  183. }
  184. }
  185. }
  186. enum PumpOptionsForOnboardingUnits: String, Equatable, CaseIterable, Identifiable {
  187. case minimed
  188. case omnipodEros
  189. case omnipodDash
  190. case dana
  191. var id: String { rawValue }
  192. var displayName: String {
  193. switch self {
  194. case .minimed:
  195. return "Medtronic 5xx / 7xx"
  196. case .omnipodEros:
  197. return "Omnipod Eros"
  198. case .omnipodDash:
  199. return "Omnipod Dash"
  200. case .dana:
  201. return "Dana (RS/-i)"
  202. }
  203. }
  204. }
  205. /// Model that holds the data collected during onboarding.
  206. extension Onboarding {
  207. @Observable final class StateModel: BaseStateModel<Provider> {
  208. @ObservationIgnored @Injected() var fileStorage: FileStorage!
  209. @ObservationIgnored @Injected() var deviceManager: DeviceDataManager!
  210. @ObservationIgnored @Injected() private var broadcaster: Broadcaster!
  211. private let settingsProvider = PickerSettingsProvider.shared
  212. // Carb Ratio related
  213. var carbRatioItems: [CarbRatioEditor.Item] = []
  214. var initialCarbRatioItems: [CarbRatioEditor.Item] = []
  215. let carbRatioTimeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
  216. let carbRatioRateValues = stride(from: 30.0, to: 501.0, by: 1.0).map { ($0.decimal ?? .zero) / 10 }
  217. // Basal Profile related
  218. var initialBasalProfileItems: [BasalProfileEditor.Item] = []
  219. var basalProfileItems: [BasalProfileEditor.Item] = []
  220. let basalProfileTimeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
  221. var basalProfileRateValues: [Decimal] {
  222. switch pumpModel {
  223. case .dana,
  224. .minimed:
  225. return stride(from: 0.1, to: 30.0, by: 0.1).map { Decimal($0) }
  226. case .omnipodDash,
  227. .omnipodEros:
  228. return stride(from: 0.05, to: 30.0, by: 0.05).map { Decimal($0) }
  229. }
  230. }
  231. // ISF related
  232. var isfItems: [ISFEditor.Item] = []
  233. var initialISFItems: [ISFEditor.Item] = []
  234. let isfTimeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
  235. var isfRateValues: [Decimal] {
  236. var values = stride(from: 9, to: 540.01, by: 1.0).map { Decimal($0) }
  237. if units == .mmolL {
  238. values = values.filter { Int(truncating: $0 as NSNumber) % 2 == 0 }
  239. }
  240. return values
  241. }
  242. // Target related
  243. var targetItems: [TargetsEditor.Item] = []
  244. var initialTargetItems: [TargetsEditor.Item] = []
  245. let targetTimeValues = stride(from: 0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
  246. var targetRateValues: [Decimal] {
  247. let glucoseSetting = PickerSetting(value: 0, step: 1, min: 72, max: 180, type: .glucose)
  248. return settingsProvider.generatePickerValues(from: glucoseSetting, units: units)
  249. }
  250. // Basal Profile
  251. var basalRates: [BasalRateEntry] = [BasalRateEntry(startTime: 0, rate: 1.0)]
  252. // Carb Ratio
  253. var carbRatio: Decimal = 10
  254. // Insulin Sensitivity Factor
  255. var isf: Decimal = 40
  256. // Blood Glucose Units
  257. var units: GlucoseUnits = .mgdL
  258. var pumpModel: PumpOptionsForOnboardingUnits = .omnipodDash
  259. var maxBolus: Decimal = 10
  260. var maxBasal: Decimal = 2
  261. var maxIOB: Decimal = 0
  262. var maxCOB: Decimal = 120
  263. struct BasalRateEntry: Identifiable {
  264. var id = UUID()
  265. var startTime: Int // Minutes from midnight
  266. var rate: Decimal
  267. var timeFormatted: String {
  268. let hours = startTime / 60
  269. let minutes = startTime % 60
  270. return String(format: "%02d:%02d", hours, minutes)
  271. }
  272. }
  273. override func subscribe() {
  274. // TODO: why are we immediately storing to settings?
  275. // saveOnboardingData()
  276. }
  277. func saveOnboardingData() {
  278. applyToSettings()
  279. applyToPreferences()
  280. applyToPumpSettings()
  281. }
  282. /// Applies the onboarding data to the app's settings.
  283. func applyToSettings() {
  284. // Make a copy of the current settings that we can mutate
  285. var settingsCopy = settingsManager.settings
  286. settingsCopy.units = units
  287. // Store therapy settings
  288. saveTargets()
  289. saveBasalProfile()
  290. saveCarbRatios()
  291. saveISFValues()
  292. // We'll directly set the settings property which will trigger the didSet observer
  293. settingsManager.settings = settingsCopy
  294. }
  295. func applyToPreferences() {
  296. var preferencesCopy = settingsManager.preferences
  297. preferencesCopy.maxIOB = maxIOB
  298. preferencesCopy.maxCOB = maxCOB
  299. // We'll directly set the preferences property which will trigger the didSet observer
  300. settingsManager.preferences = preferencesCopy
  301. }
  302. func applyToPumpSettings() {
  303. let defaultDIA = settingsProvider.settings.insulinPeakTime.value
  304. let pumpSettings = PumpSettings(insulinActionCurve: defaultDIA, maxBolus: maxBolus, maxBasal: maxBasal)
  305. fileStorage.save(pumpSettings, as: OpenAPS.Settings.settings)
  306. // TODO: is this actually necessary at this point? Nothing is set up yet, nothing is subscribed to this observer...
  307. DispatchQueue.main.async {
  308. self.broadcaster.notify(PumpSettingsObserver.self, on: DispatchQueue.main) {
  309. $0.pumpSettingsDidChange(pumpSettings)
  310. }
  311. }
  312. }
  313. // TODO: clean up these function and unify them
  314. func getTargetTherapyItems(from targets: [TargetsEditor.Item]) -> [TherapySettingItem] {
  315. targets.map {
  316. TherapySettingItem(
  317. id: UUID(),
  318. time: targetTimeValues[$0.timeIndex],
  319. value: Double(targetRateValues[$0.lowIndex])
  320. )
  321. }
  322. }
  323. func updateTargets(from therapyItems: [TherapySettingItem]) {
  324. targetItems = therapyItems.map { item in
  325. let timeIndex = targetTimeValues.firstIndex(where: { $0 == item.time }) ?? 0
  326. let closestRate = targetRateValues.enumerated().min(by: {
  327. abs(Double($0.element) - item.value) < abs(Double($1.element) - item.value)
  328. })?.offset ?? 0
  329. return TargetsEditor.Item(lowIndex: closestRate, highIndex: closestRate, timeIndex: timeIndex)
  330. }
  331. }
  332. func getBasalTherapyItems(from basalRates: [BasalProfileEditor.Item]) -> [TherapySettingItem] {
  333. basalRates.map {
  334. TherapySettingItem(
  335. id: UUID(),
  336. time: basalProfileTimeValues[$0.timeIndex],
  337. value: Double(basalProfileRateValues[$0.rateIndex])
  338. )
  339. }
  340. }
  341. func updateBasalRates(from therapyItems: [TherapySettingItem]) {
  342. basalProfileItems = therapyItems.map { item in
  343. let timeIndex = basalProfileTimeValues.firstIndex(where: { $0 == item.time }) ?? 0
  344. let closestRate = basalProfileRateValues.enumerated().min(by: {
  345. abs(Double($0.element) - item.value) < abs(Double($1.element) - item.value)
  346. })?.offset ?? 0
  347. return BasalProfileEditor.Item(rateIndex: closestRate, timeIndex: timeIndex)
  348. }
  349. }
  350. func getCarbRatioTherapyItems(from carbRatios: [CarbRatioEditor.Item]) -> [TherapySettingItem] {
  351. carbRatios.map {
  352. TherapySettingItem(
  353. id: UUID(),
  354. time: carbRatioTimeValues[$0.timeIndex],
  355. value: Double(carbRatioRateValues[$0.rateIndex])
  356. )
  357. }
  358. }
  359. func updateCarbRatios(from therapyItems: [TherapySettingItem]) {
  360. carbRatioItems = therapyItems.map { item in
  361. let timeIndex = carbRatioTimeValues.firstIndex(where: { $0 == item.time }) ?? 0
  362. let closestRate = carbRatioRateValues.enumerated().min(by: {
  363. abs(Double($0.element) - item.value) < abs(Double($1.element) - item.value)
  364. })?.offset ?? 0
  365. return CarbRatioEditor.Item(rateIndex: closestRate, timeIndex: timeIndex)
  366. }
  367. }
  368. func getSensitivityTherapyItems(from sensitivities: [ISFEditor.Item]) -> [TherapySettingItem] {
  369. sensitivities.map {
  370. TherapySettingItem(
  371. id: UUID(),
  372. time: isfTimeValues[$0.timeIndex],
  373. value: Double(isfRateValues[$0.rateIndex])
  374. )
  375. }
  376. }
  377. func updateSensitivies(from therapyItems: [TherapySettingItem]) {
  378. isfItems = therapyItems.map { item in
  379. let timeIndex = isfTimeValues.firstIndex(where: { $0 == item.time }) ?? 0
  380. let closestRate = isfRateValues.enumerated().min(by: {
  381. abs(Double($0.element) - item.value) < abs(Double($1.element) - item.value)
  382. })?.offset ?? 0
  383. return ISFEditor.Item(rateIndex: closestRate, timeIndex: timeIndex)
  384. }
  385. }
  386. // TODO: add update handler for all therapy items to automatically fill in time gaps and ensure schedule always starts at 00:00 and ends at 23:30
  387. }
  388. }
  389. // MARK: - Setup Carb Ratios
  390. extension Onboarding.StateModel {
  391. var carbRatiosHaveChanges: Bool {
  392. if initialCarbRatioItems.count != carbRatioItems.count {
  393. return true
  394. }
  395. for (initialItem, currentItem) in zip(initialCarbRatioItems, carbRatioItems) {
  396. if initialItem.rateIndex != currentItem.rateIndex || initialItem.timeIndex != currentItem.timeIndex {
  397. return true
  398. }
  399. }
  400. return false
  401. }
  402. func addCarbRatio() {
  403. var time = 0
  404. var rate = 0
  405. if let last = carbRatioItems.last {
  406. time = last.timeIndex + 1
  407. rate = last.rateIndex
  408. }
  409. let newItem = CarbRatioEditor.Item(rateIndex: rate, timeIndex: time)
  410. carbRatioItems.append(newItem)
  411. }
  412. func saveCarbRatios() {
  413. guard carbRatiosHaveChanges else { return }
  414. let schedule = carbRatioItems.enumerated().map { _, item -> CarbRatioEntry in
  415. let fotmatter = DateFormatter()
  416. fotmatter.timeZone = TimeZone(secondsFromGMT: 0)
  417. fotmatter.dateFormat = "HH:mm:ss"
  418. let date = Date(timeIntervalSince1970: self.carbRatioTimeValues[item.timeIndex])
  419. let minutes = Int(date.timeIntervalSince1970 / 60)
  420. let rate = self.carbRatioRateValues[item.rateIndex]
  421. return CarbRatioEntry(start: fotmatter.string(from: date), offset: minutes, ratio: rate)
  422. }
  423. let profile = CarbRatios(units: .grams, schedule: schedule)
  424. saveCarbRatioProfile(profile)
  425. initialCarbRatioItems = carbRatioItems.map { CarbRatioEditor.Item(rateIndex: $0.rateIndex, timeIndex: $0.timeIndex) }
  426. }
  427. // func validate() {
  428. // DispatchQueue.main.async {
  429. // let uniq = Array(Set(self.items))
  430. // let sorted = uniq.sorted { $0.timeIndex < $1.timeIndex }
  431. // sorted.first?.timeIndex = 0
  432. // if self.items != sorted {
  433. // self.items = sorted
  434. // }
  435. // }
  436. // }
  437. func saveCarbRatioProfile(_ profile: CarbRatios) {
  438. fileStorage.save(profile, as: OpenAPS.Settings.carbRatios)
  439. }
  440. }
  441. // MARK: - Setup glucose targets
  442. extension Onboarding.StateModel {
  443. var targetsHaveChanged: Bool {
  444. initialTargetItems != targetItems
  445. }
  446. func addTarget() {
  447. var time = 0
  448. var low = 0
  449. var high = 0
  450. if let last = targetItems.last {
  451. time = last.timeIndex + 1
  452. low = last.lowIndex
  453. high = low
  454. }
  455. let newItem = TargetsEditor.Item(lowIndex: low, highIndex: high, timeIndex: time)
  456. targetItems.append(newItem)
  457. }
  458. func saveTargets() {
  459. guard targetsHaveChanged else { return }
  460. let targets = targetItems.map { item -> BGTargetEntry in
  461. let formatter = DateFormatter()
  462. formatter.timeZone = TimeZone(secondsFromGMT: 0)
  463. formatter.dateFormat = "HH:mm:ss"
  464. let date = Date(timeIntervalSince1970: self.targetTimeValues[item.timeIndex])
  465. let minutes = Int(date.timeIntervalSince1970 / 60)
  466. let low = self.isfRateValues[item.lowIndex]
  467. let high = low
  468. return BGTargetEntry(low: low, high: high, start: formatter.string(from: date), offset: minutes)
  469. }
  470. let profile = BGTargets(units: .mgdL, userPreferredUnits: .mgdL, targets: targets)
  471. saveTargets(profile)
  472. initialTargetItems = targetItems
  473. .map { TargetsEditor.Item(lowIndex: $0.lowIndex, highIndex: $0.highIndex, timeIndex: $0.timeIndex) }
  474. }
  475. // func validateTarget() {
  476. // DispatchQueue.main.async {
  477. // let uniq = Array(Set(self.items))
  478. // let sorted = uniq.sorted { $0.timeIndex < $1.timeIndex }
  479. // .map { item -> Item in
  480. // Item(lowIndex: item.lowIndex, highIndex: item.highIndex, timeIndex: item.timeIndex)
  481. // }
  482. // sorted.first?.timeIndex = 0
  483. // self.items = sorted
  484. //
  485. // if self.items.isEmpty {
  486. // self.units = self.settingsManager.settings.units
  487. // }
  488. // }
  489. // }
  490. func saveTargets(_ profile: BGTargets) {
  491. fileStorage.save(profile, as: OpenAPS.Settings.bgTargets)
  492. }
  493. }
  494. // MARK: - Setup ISF values
  495. extension Onboarding.StateModel {
  496. var isfValuesHaveChanges: Bool {
  497. initialISFItems != isfItems
  498. }
  499. func addISFValue() {
  500. var time = 0
  501. var rate = 0
  502. if let last = isfItems.last {
  503. time = last.timeIndex + 1
  504. rate = last.rateIndex
  505. }
  506. let newItem = ISFEditor.Item(rateIndex: rate, timeIndex: time)
  507. isfItems.append(newItem)
  508. }
  509. func saveISFValues() {
  510. guard isfValuesHaveChanges else { return }
  511. let sensitivities = isfItems.map { item -> InsulinSensitivityEntry in
  512. let fotmatter = DateFormatter()
  513. fotmatter.timeZone = TimeZone(secondsFromGMT: 0)
  514. fotmatter.dateFormat = "HH:mm:ss"
  515. let date = Date(timeIntervalSince1970: self.isfTimeValues[item.timeIndex])
  516. let minutes = Int(date.timeIntervalSince1970 / 60)
  517. let rate = self.isfRateValues[item.rateIndex]
  518. return InsulinSensitivityEntry(sensitivity: rate, offset: minutes, start: fotmatter.string(from: date))
  519. }
  520. let profile = InsulinSensitivities(
  521. units: .mgdL,
  522. userPreferredUnits: .mgdL,
  523. sensitivities: sensitivities
  524. )
  525. saveISFProfile(profile)
  526. initialISFItems = isfItems.map { ISFEditor.Item(rateIndex: $0.rateIndex, timeIndex: $0.timeIndex) }
  527. }
  528. // func validate() {
  529. // DispatchQueue.main.async {
  530. // DispatchQueue.main.async {
  531. // let uniq = Array(Set(self.items))
  532. // let sorted = uniq.sorted { $0.timeIndex < $1.timeIndex }
  533. // sorted.first?.timeIndex = 0
  534. // if self.items != sorted {
  535. // self.items = sorted
  536. // }
  537. // if self.items.isEmpty {
  538. // self.units = self.settingsManager.settings.units
  539. // }
  540. // }
  541. // }
  542. // }
  543. func saveISFProfile(_ profile: InsulinSensitivities) {
  544. fileStorage.save(profile, as: OpenAPS.Settings.insulinSensitivities)
  545. }
  546. }
  547. // MARK: - Setup Basal Profile
  548. extension Onboarding.StateModel {
  549. var hasBasalProfileChanges: Bool {
  550. if initialBasalProfileItems.count != basalProfileItems.count {
  551. return true
  552. }
  553. for (initialItem, currentItem) in zip(initialBasalProfileItems, basalProfileItems) {
  554. if initialItem.rateIndex != currentItem.rateIndex || initialItem.timeIndex != currentItem.timeIndex {
  555. return true
  556. }
  557. }
  558. return false
  559. }
  560. func addBasalRate() {
  561. var time = 0
  562. var rate = 20 // Default to 1.0 U/h (index 20 if basalProfileRateValues starts at 0.05 and increments by 0.05)
  563. if let last = basalProfileItems.last {
  564. time = last.timeIndex + 1
  565. rate = last.rateIndex
  566. }
  567. let newItem = BasalProfileEditor.Item(rateIndex: rate, timeIndex: time)
  568. basalProfileItems.append(newItem)
  569. }
  570. func saveBasalProfile() {
  571. let profile = basalProfileItems.map { item -> BasalProfileEntry in
  572. let formatter = DateFormatter()
  573. formatter.timeZone = TimeZone(secondsFromGMT: 0)
  574. formatter.dateFormat = "HH:mm:ss"
  575. let date = Date(timeIntervalSince1970: self.basalProfileTimeValues[item.timeIndex])
  576. let minutes = Int(date.timeIntervalSince1970 / 60)
  577. let rate = self.basalProfileRateValues[item.rateIndex]
  578. return BasalProfileEntry(start: formatter.string(from: date), minutes: minutes, rate: rate)
  579. }
  580. fileStorage.save(profile, as: OpenAPS.Settings.basalProfile)
  581. }
  582. }