TidepoolTherapySettingsTests.swift 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584
  1. import CryptoKit
  2. import HealthKit
  3. import LoopKit
  4. import Testing
  5. import TidepoolKit
  6. @testable import TidepoolServiceKit
  7. @testable import Trio
  8. // Both Trio and TidepoolServiceKit define mgPerDL,
  9. // causing ambiguity. Use HealthKit's native API to avoid the conflict.
  10. private let mgPerDL = HKUnit.gramUnit(with: .milli).unitDivided(by: HKUnit.literUnit(with: .deci))
  11. private let mmolPerL = HKUnit.moleUnit(with: .milli, molarMass: HKUnitMolarMassBloodGlucose).unitDivided(by: .liter())
  12. // MARK: - StoredSettings → Tidepool Datum Tests
  13. /// Tests that verify Trio's StoredSettings correctly converts to Tidepool's pumpSettings datum.
  14. /// These test the REAL TidepoolServiceKit conversion code path.
  15. @Suite("StoredSettings Tidepool Format Tests") struct StoredSettingsTidepoolFormatTests {
  16. private static let encoder: JSONEncoder = {
  17. let encoder = JSONEncoder.tidepool
  18. encoder.outputFormatting.insert(.prettyPrinted)
  19. encoder.outputFormatting.insert(.sortedKeys)
  20. return encoder
  21. }()
  22. // MARK: - JSON Format
  23. @Test("Pump settings JSON contains required fields") func pumpSettingsJSONFormat() {
  24. let datum = StoredSettings.test.datumPumpSettings(for: "trio-user-123", hostIdentifier: "Trio", hostVersion: "0.6.0")
  25. let data = try! Self.encoder.encode(datum)
  26. let json = String(data: data, encoding: .utf8)!
  27. let requiredFields = [
  28. "\"type\" : \"pumpSettings\"",
  29. "\"activeSchedule\" : \"Default\"",
  30. "\"basalSchedules\"",
  31. "\"bgTargets\"",
  32. "\"carbRatios\"",
  33. "\"insulinSensitivities\"",
  34. "\"automatedDelivery\"",
  35. "\"name\" : \"Trio\"",
  36. "\"version\" : \"0.6.0\""
  37. ]
  38. for field in requiredFields {
  39. #expect(json.contains(field), "Missing required field: \(field)")
  40. }
  41. }
  42. @Test("Pump settings with minimal data") func pumpSettingsWithMinimalData() {
  43. let datum = StoredSettings.minimal.datumPumpSettings(for: "test-user", hostIdentifier: "Trio", hostVersion: "0.6.0")
  44. #expect(datum.activeScheduleName == "Default")
  45. #expect(datum.origin?.name == "Trio")
  46. #expect(datum.origin?.version == "0.6.0")
  47. }
  48. // MARK: - Schedule Naming
  49. @Test("All schedules use 'Default' name") func scheduleNaming() {
  50. let datum = StoredSettings.test.datumPumpSettings(for: "test", hostIdentifier: "Trio", hostVersion: "0.6.0")
  51. #expect(datum.activeScheduleName == "Default")
  52. #expect(datum.basalRateSchedules?.keys.count == 1)
  53. #expect(datum.basalRateSchedules?["Default"] != nil)
  54. #expect(datum.bloodGlucoseTargetSchedules?["Default"] != nil)
  55. #expect(datum.carbohydrateRatioSchedules?["Default"] != nil)
  56. #expect(datum.insulinSensitivitySchedules?["Default"] != nil)
  57. }
  58. // MARK: - Device Metadata
  59. @Test("Pump device metadata is included") func pumpDeviceMetadata() {
  60. let pumpDevice = HKDevice(
  61. name: "Omnipod", manufacturer: "Insulet", model: "Dash",
  62. hardwareVersion: "1.0", firmwareVersion: "2.9.0", softwareVersion: nil,
  63. localIdentifier: "pod-123", udiDeviceIdentifier: nil
  64. )
  65. let settings = makeSettings(pumpDevice: pumpDevice)
  66. let data = try! Self.encoder.encode(
  67. settings.datumPumpSettings(for: "test", hostIdentifier: "Trio", hostVersion: "0.6.0")
  68. )
  69. let json = String(data: data, encoding: .utf8)!
  70. #expect(json.contains("Omnipod"), "Missing pump device name")
  71. #expect(json.contains("Insulet"), "Missing pump manufacturer")
  72. }
  73. @Test("CGM device metadata is included") func cgmDeviceMetadata() {
  74. let cgmDevice = HKDevice(
  75. name: "Dexcom G7", manufacturer: "Dexcom", model: "G7",
  76. hardwareVersion: nil, firmwareVersion: "1.2.3", softwareVersion: "4.5.6",
  77. localIdentifier: "CGM123", udiDeviceIdentifier: nil
  78. )
  79. let settings = makeSettings(cgmDevice: cgmDevice)
  80. let datum = settings.datumCGMSettings(for: "test", hostIdentifier: "Trio", hostVersion: "0.6.0")
  81. let data = try! Self.encoder.encode(datum)
  82. let json = String(data: data, encoding: .utf8)!
  83. #expect(json.contains("Dexcom G7"), "Missing CGM device name")
  84. #expect(json.contains("Dexcom"), "Missing CGM manufacturer")
  85. }
  86. // MARK: - Suspend Threshold
  87. @Test("Suspend threshold value is preserved") func suspendThreshold() {
  88. let settings = makeSettings(
  89. suspendThreshold: GlucoseThreshold(unit: mgPerDL, value: 70.0)
  90. )
  91. let datum = settings.datumPumpSettings(for: "test", hostIdentifier: "Trio", hostVersion: "0.6.0")
  92. #expect(datum.bloodGlucoseSafetyLimit == 70, "Suspend threshold value should match")
  93. }
  94. @Test("Suspend threshold in mg/dL passes through for mmol/L user") func suspendThresholdMmolLUser() {
  95. // threshold_setting is always stored in mg/dL even when user displays mmol/L.
  96. // The adapter creates GlucoseThreshold in mg/dL; TidepoolServiceKit converts internally.
  97. let settings = makeSettings(
  98. suspendThreshold: GlucoseThreshold(unit: mgPerDL, value: 70.0),
  99. bgUnit: mmolPerL
  100. )
  101. let datum = settings.datumPumpSettings(for: "test", hostIdentifier: "Trio", hostVersion: "0.6.0")
  102. #expect(
  103. datum.bloodGlucoseSafetyLimit == 70,
  104. "Threshold in mg/dL should pass through correctly regardless of display unit"
  105. )
  106. }
  107. // MARK: - Max Basal / Max Bolus
  108. @Test("Maximum basal and bolus values are preserved") func maximumValues() {
  109. let settings = makeSettings(maxBasal: 30.0, maxBolus: 25.0)
  110. let datum = settings.datumPumpSettings(for: "test", hostIdentifier: "Trio", hostVersion: "0.6.0")
  111. #expect(datum.basal?.rateMaximum?.value == 30.0, "Max basal should handle high values")
  112. #expect(datum.bolus?.amountMaximum?.value == 25.0, "Max bolus should handle high values")
  113. }
  114. @Test("Minimum basal and bolus values are preserved") func minimumValues() {
  115. let settings = makeSettings(maxBasal: 0.5, maxBolus: 1.0)
  116. let datum = settings.datumPumpSettings(for: "test", hostIdentifier: "Trio", hostVersion: "0.6.0")
  117. #expect(datum.basal?.rateMaximum?.value == 0.5, "Should preserve low max basal")
  118. #expect(datum.bolus?.amountMaximum?.value == 1.0, "Should preserve low max bolus")
  119. }
  120. // MARK: - Automated Delivery Flag
  121. @Test("Automated delivery flag reflects dosing state") func automatedDeliveryFlag() {
  122. let enabled = makeSettings(dosingEnabled: true)
  123. let disabled = makeSettings(dosingEnabled: false)
  124. let enabledDatum = enabled.datumPumpSettings(for: "test", hostIdentifier: "Trio", hostVersion: "0.6.0")
  125. let disabledDatum = disabled.datumPumpSettings(for: "test", hostIdentifier: "Trio", hostVersion: "0.6.0")
  126. #expect(enabledDatum.automatedDelivery == true)
  127. #expect(disabledDatum.automatedDelivery == false)
  128. }
  129. // MARK: - Unit Conversion
  130. @Test("mmol/L values are converted to mg/dL by Tidepool") func mmolLUnitConversion() {
  131. let targetSchedule = GlucoseRangeSchedule(
  132. rangeSchedule: DailyQuantitySchedule(
  133. unit: mmolPerL,
  134. dailyItems: [RepeatingScheduleValue(
  135. startTime: 0,
  136. value: DoubleRange(minValue: 5.0, maxValue: 6.0)
  137. )],
  138. timeZone: .current
  139. )!,
  140. override: nil
  141. )
  142. let isfSchedule = InsulinSensitivitySchedule(
  143. unit: mmolPerL,
  144. dailyItems: [RepeatingScheduleValue(startTime: 0, value: 3.0)],
  145. timeZone: .current
  146. )
  147. let settings = makeSettings(
  148. glucoseTargetRangeSchedule: targetSchedule,
  149. insulinSensitivitySchedule: isfSchedule,
  150. bgUnit: mmolPerL
  151. )
  152. let datum = settings.datumPumpSettings(for: "test", hostIdentifier: "Trio", hostVersion: "0.6.0")
  153. // Tidepool converts to mg/dL (5.0 mmol/L ≈ 90 mg/dL)
  154. let target = datum.bloodGlucoseTargetSchedules?["Default"]?.first
  155. #expect(abs((target?.low ?? 0) - 90) <= 1)
  156. #expect(abs((target?.high ?? 0) - 108) <= 1)
  157. let isf = datum.insulinSensitivitySchedules?["Default"]?.first
  158. #expect(abs((isf?.amount ?? 0) - 54) <= 1)
  159. }
  160. // MARK: - Insulin Model
  161. @Test("Insulin model preserves DIA and peak time") func insulinModel() {
  162. let model = StoredInsulinModel(
  163. modelType: .rapidAdult,
  164. delay: .minutes(10),
  165. actionDuration: .hours(8),
  166. peakActivity: .minutes(65)
  167. )
  168. let settings = makeSettings(insulinModel: model)
  169. let datum = settings.datumPumpSettings(for: "test", hostIdentifier: "Trio", hostVersion: "0.6.0")
  170. #expect(datum.insulinModel != nil, "Insulin model should be present")
  171. #expect(datum.insulinModel?.actionDuration == .hours(8), "DIA should match user setting")
  172. #expect(datum.insulinModel?.actionPeakOffset == .minutes(65), "Peak time should match user setting")
  173. }
  174. @Test("Fiasp insulin model maps correctly") func fiaspInsulinModel() {
  175. let model = StoredInsulinModel(
  176. modelType: .fiasp,
  177. delay: .minutes(10),
  178. actionDuration: .hours(6),
  179. peakActivity: .minutes(55)
  180. )
  181. let settings = makeSettings(insulinModel: model)
  182. let datum = settings.datumPumpSettings(for: "test", hostIdentifier: "Trio", hostVersion: "0.6.0")
  183. #expect(datum.insulinModel?.modelType == .fiasp, "Ultra-rapid should map to fiasp")
  184. #expect(datum.insulinModel?.actionDuration == .hours(6))
  185. #expect(datum.insulinModel?.actionPeakOffset == .minutes(55))
  186. }
  187. // MARK: - Helpers
  188. private func makeSettings(
  189. dosingEnabled: Bool = true,
  190. glucoseTargetRangeSchedule: GlucoseRangeSchedule? = nil,
  191. insulinSensitivitySchedule: InsulinSensitivitySchedule? = nil,
  192. maxBasal: Double? = 5.0,
  193. maxBolus: Double? = 10.0,
  194. suspendThreshold: GlucoseThreshold? = nil,
  195. insulinModel: StoredInsulinModel? = nil,
  196. cgmDevice: HKDevice? = nil,
  197. pumpDevice: HKDevice? = nil,
  198. bgUnit: HKUnit = mgPerDL
  199. ) -> StoredSettings {
  200. let tz = TimeZone(secondsFromGMT: 0)!
  201. let defaultTarget = GlucoseRangeSchedule(
  202. rangeSchedule: DailyQuantitySchedule(
  203. unit: mgPerDL,
  204. dailyItems: [RepeatingScheduleValue(
  205. startTime: 0,
  206. value: DoubleRange(minValue: 100.0, maxValue: 110.0)
  207. )],
  208. timeZone: tz
  209. )!,
  210. override: nil
  211. )
  212. let defaultBasal = BasalRateSchedule(
  213. dailyItems: [RepeatingScheduleValue(startTime: 0, value: 1.0)],
  214. timeZone: tz
  215. )!
  216. let defaultISF = InsulinSensitivitySchedule(
  217. unit: mgPerDL,
  218. dailyItems: [RepeatingScheduleValue(startTime: 0, value: 45.0)],
  219. timeZone: tz
  220. )!
  221. let defaultCarb = CarbRatioSchedule(
  222. unit: .gram(),
  223. dailyItems: [RepeatingScheduleValue(startTime: 0, value: 15.0)],
  224. timeZone: tz
  225. )!
  226. return StoredSettings(
  227. date: Date(),
  228. controllerTimeZone: .current,
  229. dosingEnabled: dosingEnabled,
  230. glucoseTargetRangeSchedule: glucoseTargetRangeSchedule ?? defaultTarget,
  231. preMealTargetRange: nil,
  232. workoutTargetRange: nil,
  233. overridePresets: nil,
  234. scheduleOverride: nil,
  235. preMealOverride: nil,
  236. maximumBasalRatePerHour: maxBasal,
  237. maximumBolus: maxBolus,
  238. suspendThreshold: suspendThreshold,
  239. insulinType: nil,
  240. defaultRapidActingModel: insulinModel,
  241. basalRateSchedule: defaultBasal,
  242. insulinSensitivitySchedule: insulinSensitivitySchedule ?? defaultISF,
  243. carbRatioSchedule: defaultCarb,
  244. notificationSettings: nil,
  245. controllerDevice: nil,
  246. cgmDevice: cgmDevice,
  247. pumpDevice: pumpDevice,
  248. bloodGlucoseUnit: bgUnit,
  249. syncIdentifier: UUID()
  250. )
  251. }
  252. }
  253. // MARK: - Conversion Logic Tests
  254. /// Tests for the conversion math used in BaseTidepoolManager.
  255. /// These verify the patterns used in the real adapter code.
  256. @Suite("BaseTidepoolManager Conversion Tests") struct BaseTidepoolManagerTests {
  257. // MARK: - Basal Profile Conversion
  258. @Test("Basal profile minutes convert to seconds") func basalProfileMinutesToSeconds() {
  259. let entries: [(minutes: Int, expectedSeconds: TimeInterval)] = [
  260. (0, 0),
  261. (210, 12600),
  262. (360, 21600),
  263. (720, 43200),
  264. (1125, 67500),
  265. (1439, 86340)
  266. ]
  267. for (minutes, expected) in entries {
  268. let startTime = TimeInterval(minutes * 60)
  269. #expect(startTime == expected, "\(minutes) minutes should be \(expected) seconds")
  270. }
  271. }
  272. @Test("Basal profile uses minutes field for start time") func basalProfileUsesMinutesField() {
  273. let entries = [
  274. BasalProfileEntry(start: "00:00:00", minutes: 0, rate: 1.0),
  275. BasalProfileEntry(start: "06:00:00", minutes: 360, rate: 1.5),
  276. BasalProfileEntry(start: "12:00:00", minutes: 720, rate: 1.25)
  277. ]
  278. let items = entries.map { entry in
  279. RepeatingScheduleValue(
  280. startTime: TimeInterval(entry.minutes * 60),
  281. value: Double(entry.rate)
  282. )
  283. }
  284. let schedule = BasalRateSchedule(dailyItems: items, timeZone: .current)
  285. #expect(schedule != nil)
  286. #expect(schedule?.items[0].startTime == 0)
  287. #expect(schedule?.items[1].startTime == 21600)
  288. #expect(schedule?.items[2].startTime == 43200)
  289. }
  290. // MARK: - Carb Ratio Conversion
  291. @Test("Carb ratio offset converts to seconds") func carbRatioOffsetToSeconds() {
  292. let entries = [
  293. CarbRatioEntry(start: "00:00", offset: 0, ratio: 15.0),
  294. CarbRatioEntry(start: "06:00", offset: 360, ratio: 12.0),
  295. CarbRatioEntry(start: "12:00", offset: 720, ratio: 10.0)
  296. ]
  297. let items = entries.map { entry in
  298. RepeatingScheduleValue(
  299. startTime: TimeInterval(entry.offset * 60),
  300. value: Double(entry.ratio)
  301. )
  302. }
  303. #expect(items[0].startTime == 0)
  304. #expect(items[1].startTime == 21600)
  305. #expect(items[2].startTime == 43200)
  306. }
  307. // MARK: - ISF Conversion
  308. @Test("ISF offset converts to seconds") func insulinSensitivityOffsetToSeconds() {
  309. let entries = [
  310. InsulinSensitivityEntry(sensitivity: 50.0, offset: 0, start: "00:00"),
  311. InsulinSensitivityEntry(sensitivity: 45.0, offset: 480, start: "08:00")
  312. ]
  313. let items = entries.map { entry in
  314. RepeatingScheduleValue(
  315. startTime: TimeInterval(entry.offset * 60),
  316. value: Double(entry.sensitivity)
  317. )
  318. }
  319. #expect(items[0].startTime == 0)
  320. #expect(items[1].startTime == 28800, "480 min = 28800 sec")
  321. }
  322. // MARK: - BG Target Conversion
  323. @Test("BG target offset converts to seconds") func bgTargetOffsetToSeconds() {
  324. let entries = [
  325. BGTargetEntry(low: 100, high: 110, start: "00:00", offset: 0),
  326. BGTargetEntry(low: 110, high: 120, start: "22:00", offset: 1320)
  327. ]
  328. #expect(TimeInterval(entries[0].offset * 60) == 0)
  329. #expect(TimeInterval(entries[1].offset * 60) == 79200, "1320 min = 79200 sec")
  330. }
  331. @Test("BG target low and high values are preserved") func bgTargetLowHighValues() {
  332. let entry = BGTargetEntry(low: 90, high: 120, start: "00:00", offset: 0)
  333. #expect(Double(entry.low) == 90)
  334. #expect(Double(entry.high) == 120)
  335. }
  336. // MARK: - Insulin Model Conversion
  337. @Test("Preset peak times match expected values when custom peak disabled") func presetPeakTimes() {
  338. // When useCustomPeakTime is false, should use LoopKit preset defaults
  339. let rapidAdultPeak = ExponentialInsulinModelPreset.rapidActingAdult.peakActivity
  340. let fiaspPeak = ExponentialInsulinModelPreset.fiasp.peakActivity
  341. #expect(rapidAdultPeak == .minutes(75), "rapidActingAdult preset peak should be 75 min")
  342. #expect(fiaspPeak == .minutes(55), "fiasp preset peak should be 55 min")
  343. }
  344. @Test("Custom peak time range boundaries") func customPeakTimeRange() {
  345. // insulinPeakTime picker: min 35, max 120, step 1 (minutes)
  346. let minPeak: TimeInterval = .minutes(35)
  347. let maxPeak: TimeInterval = .minutes(120)
  348. #expect(minPeak == 2100, "35 minutes = 2100 seconds")
  349. #expect(maxPeak == 7200, "120 minutes = 7200 seconds")
  350. }
  351. @Test("DIA range boundaries") func diaRange() {
  352. // insulinActionCurve picker: min 5, max 10, step 0.5 (hours)
  353. let minDIA: TimeInterval = .hours(5)
  354. let maxDIA: TimeInterval = .hours(10)
  355. #expect(minDIA == 18000, "5 hours = 18000 seconds")
  356. #expect(maxDIA == 36000, "10 hours = 36000 seconds")
  357. }
  358. // MARK: - Content-Based Sync Identifier
  359. @Test("Same settings produce the same sync identifier") func syncIdentifierDeterminism() {
  360. let id1 = computeTestSyncId(maxBasal: "5.0", maxBolus: "10.0", dosingEnabled: true)
  361. let id2 = computeTestSyncId(maxBasal: "5.0", maxBolus: "10.0", dosingEnabled: true)
  362. #expect(id1 == id2, "Same settings should produce the same sync identifier")
  363. }
  364. @Test("Different settings produce different sync identifiers") func syncIdentifierChanges() {
  365. let baseline = computeTestSyncId(maxBasal: "5.0", maxBolus: "10.0", dosingEnabled: true)
  366. let changedBasal = computeTestSyncId(maxBasal: "6.0", maxBolus: "10.0", dosingEnabled: true)
  367. let changedDosing = computeTestSyncId(maxBasal: "5.0", maxBolus: "10.0", dosingEnabled: false)
  368. #expect(baseline != changedBasal, "Different maxBasal should produce different ID")
  369. #expect(baseline != changedDosing, "Different dosingEnabled should produce different ID")
  370. #expect(changedBasal != changedDosing, "All three should be unique")
  371. }
  372. // MARK: - Helpers
  373. /// Replicates the SHA-256 hash algorithm from BaseTidepoolManager.contentBasedSyncIdentifier
  374. private func computeTestSyncId(maxBasal: String, maxBolus: String, dosingEnabled: Bool) -> UUID {
  375. var hasher = SHA256()
  376. hasher.update(data: Data("0:1.0".utf8)) // basal entry
  377. hasher.update(data: Data("0:15".utf8)) // carb ratio
  378. hasher.update(data: Data("0:50".utf8)) // ISF
  379. hasher.update(data: Data("0:100:110".utf8)) // BG target
  380. hasher.update(data: Data("maxBasal:\(maxBasal)".utf8))
  381. hasher.update(data: Data("maxBolus:\(maxBolus)".utf8))
  382. hasher.update(data: Data("threshold:100".utf8))
  383. hasher.update(data: Data("dosingEnabled:\(dosingEnabled)".utf8))
  384. let digest = hasher.finalize()
  385. let bytes = Array(digest.prefix(16))
  386. return UUID(uuid: (
  387. bytes[0], bytes[1], bytes[2], bytes[3],
  388. bytes[4], bytes[5], bytes[6], bytes[7],
  389. bytes[8], bytes[9], bytes[10], bytes[11],
  390. bytes[12], bytes[13], bytes[14], bytes[15]
  391. ))
  392. }
  393. }
  394. // MARK: - Test Fixtures
  395. private extension StoredSettings {
  396. static var test: StoredSettings {
  397. let tz = TimeZone(secondsFromGMT: 0)!
  398. let pumpDevice = HKDevice(
  399. name: "Omnipod", manufacturer: "Insulet", model: "Dash",
  400. hardwareVersion: "1.0", firmwareVersion: "2.9.0", softwareVersion: nil,
  401. localIdentifier: "pod-serial-123", udiDeviceIdentifier: nil
  402. )
  403. return StoredSettings(
  404. date: Date(),
  405. controllerTimeZone: TimeZone(identifier: "America/Los_Angeles")!,
  406. dosingEnabled: true,
  407. glucoseTargetRangeSchedule: GlucoseRangeSchedule(
  408. rangeSchedule: DailyQuantitySchedule(
  409. unit: mgPerDL,
  410. dailyItems: [RepeatingScheduleValue(startTime: 0, value: DoubleRange(minValue: 100.0, maxValue: 110.0))],
  411. timeZone: tz
  412. )!,
  413. override: nil
  414. ),
  415. preMealTargetRange: nil,
  416. workoutTargetRange: nil,
  417. overridePresets: nil,
  418. scheduleOverride: nil,
  419. preMealOverride: nil,
  420. maximumBasalRatePerHour: 5.0,
  421. maximumBolus: 10.0,
  422. suspendThreshold: nil,
  423. insulinType: .humalog,
  424. defaultRapidActingModel: nil,
  425. basalRateSchedule: BasalRateSchedule(dailyItems: [
  426. RepeatingScheduleValue(startTime: 0, value: 1.0),
  427. RepeatingScheduleValue(startTime: 21600, value: 1.5),
  428. RepeatingScheduleValue(startTime: 43200, value: 1.25),
  429. RepeatingScheduleValue(startTime: 64800, value: 1.0)
  430. ], timeZone: tz)!,
  431. insulinSensitivitySchedule: InsulinSensitivitySchedule(
  432. unit: mgPerDL,
  433. dailyItems: [RepeatingScheduleValue(startTime: 0, value: 45.0)],
  434. timeZone: tz
  435. )!,
  436. carbRatioSchedule: CarbRatioSchedule(
  437. unit: .gram(),
  438. dailyItems: [RepeatingScheduleValue(startTime: 0, value: 15.0)],
  439. timeZone: tz
  440. )!,
  441. notificationSettings: nil,
  442. controllerDevice: nil,
  443. cgmDevice: nil,
  444. pumpDevice: pumpDevice,
  445. bloodGlucoseUnit: mgPerDL,
  446. syncIdentifier: UUID()
  447. )
  448. }
  449. static var minimal: StoredSettings {
  450. let tz = TimeZone(secondsFromGMT: 0)!
  451. return StoredSettings(
  452. date: Date(),
  453. controllerTimeZone: .current,
  454. dosingEnabled: true,
  455. glucoseTargetRangeSchedule: GlucoseRangeSchedule(
  456. rangeSchedule: DailyQuantitySchedule(
  457. unit: mgPerDL,
  458. dailyItems: [RepeatingScheduleValue(startTime: 0, value: DoubleRange(minValue: 100.0, maxValue: 110.0))],
  459. timeZone: tz
  460. )!,
  461. override: nil
  462. ),
  463. preMealTargetRange: nil,
  464. workoutTargetRange: nil,
  465. overridePresets: nil,
  466. scheduleOverride: nil,
  467. preMealOverride: nil,
  468. maximumBasalRatePerHour: nil,
  469. maximumBolus: nil,
  470. suspendThreshold: nil,
  471. insulinType: nil,
  472. defaultRapidActingModel: nil,
  473. basalRateSchedule: BasalRateSchedule(
  474. dailyItems: [RepeatingScheduleValue(startTime: 0, value: 1.0)],
  475. timeZone: tz
  476. )!,
  477. insulinSensitivitySchedule: InsulinSensitivitySchedule(
  478. unit: mgPerDL,
  479. dailyItems: [RepeatingScheduleValue(startTime: 0, value: 45.0)],
  480. timeZone: tz
  481. )!,
  482. carbRatioSchedule: CarbRatioSchedule(
  483. unit: .gram(),
  484. dailyItems: [RepeatingScheduleValue(startTime: 0, value: 15.0)],
  485. timeZone: tz
  486. )!,
  487. notificationSettings: nil,
  488. controllerDevice: nil,
  489. cgmDevice: nil,
  490. pumpDevice: nil,
  491. bloodGlucoseUnit: mgPerDL,
  492. syncIdentifier: UUID()
  493. )
  494. }
  495. }