DetermineBasalGenerator.swift 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643
  1. import Foundation
  2. protocol OverrideHandler {
  3. func overrideProfileParameters(profile: Profile, override: Override?) throws -> Profile
  4. // TODO: handle mutation of profile parameters that the user can alter using Overrides
  5. /// This could also possibly be handled via an extension of our existing `ProfileGenerator` (?)
  6. }
  7. enum DeterminationGenerator {
  8. // override data can just be fetched from the DB
  9. // handling via overrideManager ?
  10. /// Top-level determination generator, callers should use this function
  11. static func generate(
  12. profile: Profile,
  13. preferences: Preferences,
  14. currentTemp: TempBasal,
  15. iobData: [IobResult],
  16. mealData: ComputedCarbs,
  17. autosensData: Autosens,
  18. reservoirData: Decimal,
  19. glucose: [BloodGlucose],
  20. trioCustomOrefVariables: TrioCustomOrefVariables,
  21. currentTime: Date
  22. ) throws -> Determination? {
  23. let glucoseStatus = try Self.getGlucoseStatus(glucoseReadings: glucose)
  24. guard let glucoseStatus = glucoseStatus else { throw DeterminationError.missingInputs }
  25. return try determineBasal(
  26. profile: profile,
  27. preferences: preferences,
  28. currentTemp: currentTemp,
  29. iobData: iobData,
  30. mealData: mealData,
  31. autosensData: autosensData,
  32. reservoirData: reservoirData,
  33. glucoseStatus: glucoseStatus,
  34. trioCustomOrefVariables: trioCustomOrefVariables,
  35. currentTime: currentTime
  36. )
  37. }
  38. /// Internal function to implement the core determine basal logic. We have a separate function
  39. /// from `generate` so that we can pass GlucoseStatus values directly into the function
  40. /// for testing.
  41. static func determineBasal(
  42. profile: Profile,
  43. preferences: Preferences,
  44. currentTemp: TempBasal,
  45. iobData: [IobResult],
  46. mealData: ComputedCarbs,
  47. autosensData: Autosens,
  48. reservoirData _: Decimal,
  49. glucoseStatus: GlucoseStatus,
  50. trioCustomOrefVariables: TrioCustomOrefVariables,
  51. currentTime: Date
  52. ) throws -> Determination? {
  53. var autosensData = autosensData
  54. try checkDeterminationInputs(
  55. glucoseStatus: glucoseStatus,
  56. currentTemp: currentTemp,
  57. iobData: iobData,
  58. profile: profile,
  59. trioCustomOrefVariables: trioCustomOrefVariables,
  60. currentTime: currentTime,
  61. )
  62. let currentGlucose: Decimal = glucoseStatus.glucose
  63. if let errorDetermination = try handleTempBasalCases(
  64. glucoseStatus: glucoseStatus,
  65. profile: profile,
  66. currentTemp: currentTemp,
  67. currentTime: currentTime
  68. ) {
  69. return errorDetermination
  70. }
  71. // Safety check: current temp vs. last temp in iob
  72. guard let lastTempTarget = iobData.first?.lastTemp else {
  73. throw DeterminationError.missingIob
  74. }
  75. if !checkCurrentTempBasalRateSafety(
  76. currentTemp: currentTemp,
  77. lastTempTarget: lastTempTarget,
  78. currentTime: currentTime
  79. ) {
  80. let reason =
  81. "Safety check: currentTemp does not match lastTemp in IOB or lastTemp ended long ago; canceling temp basal."
  82. return Determination(
  83. id: UUID(),
  84. reason: reason,
  85. units: nil,
  86. insulinReq: nil,
  87. eventualBG: nil,
  88. sensitivityRatio: nil,
  89. rate: 0,
  90. duration: 0,
  91. iob: iobData.first?.iob,
  92. cob: nil,
  93. predictions: nil,
  94. deliverAt: currentTime,
  95. carbsReq: nil,
  96. temp: .absolute,
  97. bg: glucoseStatus.glucose,
  98. reservoir: nil,
  99. isf: profile.sens,
  100. timestamp: currentTime,
  101. tdd: nil,
  102. current_target: profile.targetBg,
  103. minDelta: nil,
  104. expectedDelta: nil,
  105. minGuardBG: nil,
  106. minPredBG: nil,
  107. threshold: nil,
  108. carbRatio: profile.carbRatio,
  109. received: false
  110. )
  111. }
  112. let dynamicIsfResult = DynamicISF.calculate(
  113. profile: profile,
  114. preferences: preferences,
  115. currentGlucose: currentGlucose,
  116. trioCustomOrefVariables: trioCustomOrefVariables
  117. )
  118. if let dynamicIsfResult = dynamicIsfResult {
  119. autosensData = Autosens(
  120. ratio: dynamicIsfResult.ratio,
  121. newisf: autosensData.newisf,
  122. deviationsUnsorted: autosensData.deviationsUnsorted,
  123. timestamp: autosensData.timestamp
  124. )
  125. }
  126. let (sensitivityRatio, updateAutosensRatio) = calculateSensitivityRatio(
  127. currentGlucose: currentGlucose,
  128. profile: profile,
  129. autosens: autosensData,
  130. targetGlucose: profile.profileTarget(trioCustomOrefVariables: trioCustomOrefVariables) ?? 120,
  131. temptargetSet: profile.temptargetSet ?? false,
  132. dynamicIsfResult: dynamicIsfResult
  133. )
  134. if updateAutosensRatio {
  135. autosensData = Autosens(
  136. ratio: sensitivityRatio,
  137. newisf: autosensData.newisf,
  138. deviationsUnsorted: autosensData.deviationsUnsorted,
  139. timestamp: autosensData.timestamp
  140. )
  141. }
  142. let basal: Decimal
  143. if let dynamicIsfResult = dynamicIsfResult, profile.tddAdjBasal {
  144. basal = computeAdjustedBasal(
  145. currentBasalRate: profile.currentBasal ?? profile.basalFor(time: currentTime),
  146. sensitivityRatio: dynamicIsfResult.tddRatio
  147. )
  148. } else {
  149. basal = computeAdjustedBasal(
  150. currentBasalRate: profile.currentBasal ?? profile.basalFor(time: currentTime),
  151. sensitivityRatio: sensitivityRatio
  152. )
  153. }
  154. // this is the `sens` variable in JS, it's the adjusted sensitivity
  155. let adjustedSensitivity = computeAdjustedSensitivity(
  156. sensitivity: profile.sens ?? profile.sensitivityFor(time: currentTime),
  157. sensitivityRatio: sensitivityRatio,
  158. trioCustomOrefVariables: trioCustomOrefVariables
  159. )
  160. let (adjustedGlucoseTargets, threshold) = adjustGlucoseTargets(
  161. profile: profile,
  162. autosens: autosensData,
  163. trioCustomOrefVariables: trioCustomOrefVariables,
  164. temptargetSet: profile.temptargetSet ?? false,
  165. targetGlucose: profile.minBg ?? 100,
  166. minGlucose: profile.minBg ?? 70, // TODO: can we force unwrap?
  167. maxGlucose: profile.maxBg ?? 180,
  168. noise: 1
  169. )
  170. let glucoseImpactSeries = buildGlucoseImpactSeries(iobDataSeries: iobData, sensitivity: adjustedSensitivity)
  171. let glucoseImpactSeriesWithZeroTemp = buildGlucoseImpactSeries(
  172. iobDataSeries: iobData,
  173. sensitivity: adjustedSensitivity,
  174. withZeroTemp: true
  175. )
  176. guard let currentGlucoseImpact = glucoseImpactSeries.first?.jsRounded(scale: 2) else {
  177. throw DeterminationError.determinationError
  178. }
  179. let minDelta = min(glucoseStatus.delta, glucoseStatus.shortAvgDelta)
  180. let minAvgDelta = min(glucoseStatus.shortAvgDelta, glucoseStatus.longAvgDelta)
  181. let longAvgDelta = glucoseStatus.longAvgDelta
  182. let intervals: Decimal = 6 // 30 / 5
  183. var deviation = (intervals * (minDelta - currentGlucoseImpact)).rounded(toPlaces: 0)
  184. if deviation < 0 {
  185. deviation = (intervals * (minAvgDelta - currentGlucoseImpact)).rounded(toPlaces: 0)
  186. if deviation < 0 {
  187. deviation = (intervals * (longAvgDelta - currentGlucoseImpact)).rounded(toPlaces: 0)
  188. }
  189. }
  190. // Calculate what oref calls "naive eventual glucose"
  191. guard let currentIob = iobData.first?.iob else {
  192. throw DeterminationError.missingIob
  193. }
  194. let naiveEventualGlucose: Decimal
  195. if currentIob > 0 {
  196. naiveEventualGlucose = (currentGlucose - (currentIob * adjustedSensitivity)).rounded(toPlaces: 0)
  197. } else {
  198. naiveEventualGlucose =
  199. (
  200. currentGlucose -
  201. (
  202. currentIob *
  203. min(
  204. profile.profileSensitivity(at: currentTime, trioCustomOrefVaribales: trioCustomOrefVariables),
  205. adjustedSensitivity
  206. )
  207. )
  208. )
  209. .rounded(toPlaces: 0)
  210. }
  211. let eventualGlucose = naiveEventualGlucose + deviation
  212. // Safety: if we ever get an invalid Decimal (very rare with Decimal), handle
  213. guard eventualGlucose.isFinite else {
  214. throw DeterminationError.eventualGlucoseCalculationError(sensitivity: adjustedSensitivity, deviation: deviation)
  215. }
  216. let forecastResult = ForecastGenerator.generate(
  217. glucose: currentGlucose,
  218. glucoseStatus: glucoseStatus,
  219. currentGlucoseImpact: currentGlucoseImpact,
  220. glucoseImpactSeries: glucoseImpactSeries,
  221. glucoseImpactSeriesWithZeroTemp: glucoseImpactSeriesWithZeroTemp,
  222. iobData: iobData,
  223. mealData: mealData,
  224. profile: profile,
  225. preferences: preferences,
  226. trioCustomOrefVariables: trioCustomOrefVariables,
  227. dynamicIsfResult: dynamicIsfResult,
  228. targetGlucose: adjustedGlucoseTargets.targetGlucose,
  229. adjustedSensitivity: adjustedSensitivity,
  230. sensitivityRatio: sensitivityRatio,
  231. naiveEventualGlucose: naiveEventualGlucose,
  232. eventualGlucose: eventualGlucose,
  233. threshold: threshold,
  234. currentTime: currentTime
  235. )
  236. // used for pre dosing decision sanity later on
  237. let expectedDelta = calculateExpectedDelta(
  238. targetGlucose: adjustedGlucoseTargets.targetGlucose,
  239. eventualGlucose: eventualGlucose,
  240. glucoseImpact: currentGlucoseImpact
  241. )
  242. let dosingInputs = DosingEngine.prepareDosingInputs(
  243. profile: profile,
  244. mealData: mealData,
  245. forecast: forecastResult,
  246. naiveEventualGlucose: naiveEventualGlucose,
  247. threshold: threshold,
  248. glucoseImpact: currentGlucoseImpact,
  249. deviation: deviation,
  250. currentBasal: profile.currentBasal ?? profile.basalFor(time: currentTime),
  251. overrideFactor: trioCustomOrefVariables.overrideFactor(),
  252. adjustedSensitivity: adjustedSensitivity,
  253. isfReason: "", // Placeholder
  254. tddReason: "", // Placeholder
  255. targetLog: "" // Placeholder
  256. )
  257. let smbDecision = try DosingEngine.makeSMBDosingDecision(
  258. profile: profile,
  259. meal: mealData,
  260. currentGlucose: currentGlucose,
  261. adjustedTargetGlucose: adjustedGlucoseTargets.targetGlucose,
  262. minGuardGlucose: forecastResult.minGuardGlucose,
  263. threshold: threshold,
  264. glucoseStatus: glucoseStatus,
  265. trioCustomOrefVariables: trioCustomOrefVariables,
  266. clock: currentTime
  267. )
  268. let smbIsEnabled = smbDecision.isEnabled
  269. var reason = dosingInputs.reason
  270. if let smbReason = smbDecision.reason {
  271. reason += smbReason
  272. }
  273. var determination = Determination(
  274. id: UUID(),
  275. reason: reason,
  276. units: nil,
  277. insulinReq: nil,
  278. eventualBG: Int(forecastResult.eventualGlucose.jsRounded()),
  279. sensitivityRatio: sensitivityRatio, // this would only the AS-adjusted one for now
  280. rate: nil,
  281. duration: nil,
  282. iob: iobData.first?.iob,
  283. cob: mealData.mealCOB,
  284. predictions: Predictions(
  285. iob: forecastResult.iob.map { Int($0.jsRounded()) },
  286. zt: forecastResult.zt.map { Int($0.jsRounded()) },
  287. cob: forecastResult.cob?.map { Int($0.jsRounded()) },
  288. uam: forecastResult.uam?.map { Int($0.jsRounded()) }
  289. ),
  290. deliverAt: currentTime,
  291. carbsReq: dosingInputs.carbsRequired?.carbs,
  292. temp: nil,
  293. bg: currentGlucose,
  294. reservoir: nil,
  295. isf: nil,
  296. timestamp: currentTime,
  297. tdd: nil,
  298. current_target: nil,
  299. minDelta: nil,
  300. expectedDelta: expectedDelta,
  301. minGuardBG: smbDecision.minGuardGlucose ?? forecastResult.minGuardGlucose,
  302. minPredBG: forecastResult.minForecastedGlucose,
  303. threshold: threshold.jsRounded(),
  304. carbRatio: forecastResult.adjustedCarbRatio.jsRounded(scale: 1),
  305. received: false
  306. )
  307. // MARK: - Core dosing logic
  308. let (shouldSetTempBasalForLowGlucoseSuspend, lowGlucoseSuspendDetermination) = try DosingEngine.lowGlucoseSuspend(
  309. currentGlucose: currentGlucose,
  310. minGuardGlucose: forecastResult.minGuardGlucose,
  311. iob: currentIob,
  312. minDelta: minDelta,
  313. expectedDelta: expectedDelta,
  314. threshold: threshold,
  315. overrideFactor: trioCustomOrefVariables.overrideFactor(),
  316. profile: profile,
  317. adjustedSensitivity: adjustedSensitivity,
  318. targetGlucose: adjustedGlucoseTargets.targetGlucose,
  319. currentTemp: currentTemp,
  320. determination: determination
  321. )
  322. determination = lowGlucoseSuspendDetermination
  323. if shouldSetTempBasalForLowGlucoseSuspend {
  324. return determination
  325. }
  326. let (shouldSetTempBasalForSkipNeutralTemp, skipNeutralTempDetermination) = try DosingEngine.skipNeutralTempBasal(
  327. smbIsEnabled: smbIsEnabled,
  328. profile: profile,
  329. clock: currentTime,
  330. currentTemp: currentTemp,
  331. determination: determination
  332. )
  333. determination = skipNeutralTempDetermination
  334. if shouldSetTempBasalForSkipNeutralTemp {
  335. return determination
  336. }
  337. let (shouldSetTempBasalForLowEventualGlucose, lowEventualGlucoseDetermination) = try DosingEngine
  338. .handleLowEventualGlucose(
  339. eventualGlucose: forecastResult.eventualGlucose,
  340. minGlucose: adjustedGlucoseTargets.minGlucose,
  341. targetGlucose: adjustedGlucoseTargets.targetGlucose,
  342. minDelta: minDelta,
  343. expectedDelta: expectedDelta,
  344. carbsRequired: dosingInputs.carbsRequired?.carbs ?? 0,
  345. naiveEventualGlucose: naiveEventualGlucose,
  346. glucoseStatus: glucoseStatus,
  347. currentTemp: currentTemp,
  348. basal: basal,
  349. profile: profile,
  350. determination: determination,
  351. adjustedSensitivity: adjustedSensitivity,
  352. overrideFactor: trioCustomOrefVariables.overrideFactor()
  353. )
  354. determination = lowEventualGlucoseDetermination
  355. if shouldSetTempBasalForLowEventualGlucose {
  356. return determination
  357. }
  358. let (
  359. shouldSetTempBasalForGlucoseFallingFasterThanExpected,
  360. glucoseFallingFasterThanExpectedDetermination
  361. ) = try DosingEngine.glucoseFallingFasterThanExpected(
  362. eventualGlucose: forecastResult.eventualGlucose,
  363. minGlucose: adjustedGlucoseTargets.minGlucose,
  364. minDelta: minDelta,
  365. expectedDelta: expectedDelta,
  366. glucoseStatus: glucoseStatus,
  367. currentTemp: currentTemp,
  368. basal: basal,
  369. smbIsEnabled: smbIsEnabled,
  370. profile: profile,
  371. determination: determination
  372. )
  373. determination = glucoseFallingFasterThanExpectedDetermination
  374. if shouldSetTempBasalForGlucoseFallingFasterThanExpected {
  375. return determination
  376. }
  377. let (
  378. shouldSetTempBasalEventualOrForecastGlucoseLessThanMax,
  379. eventualOrForecastGlucoseLessThanMaxDetermination
  380. ) = try DosingEngine.eventualOrForecastGlucoseLessThanMax(
  381. eventualGlucose: forecastResult.eventualGlucose,
  382. maxGlucose: adjustedGlucoseTargets.maxGlucose,
  383. minForecastGlucose: forecastResult.minForecastedGlucose,
  384. currentTemp: currentTemp,
  385. basal: basal,
  386. smbIsEnabled: smbIsEnabled,
  387. profile: profile,
  388. determination: determination
  389. )
  390. determination = eventualOrForecastGlucoseLessThanMaxDetermination
  391. if shouldSetTempBasalEventualOrForecastGlucoseLessThanMax {
  392. return determination
  393. }
  394. if forecastResult.eventualGlucose >= adjustedGlucoseTargets.maxGlucose {
  395. determination
  396. .reason +=
  397. "Eventual BG \(DosingEngine.convertGlucose(profile: profile, glucose: forecastResult.eventualGlucose)) >= \(DosingEngine.convertGlucose(profile: profile, glucose: adjustedGlucoseTargets.maxGlucose)), "
  398. }
  399. let (shouldSetTempBasalForIobGreaterThanMax, iobGreaterThanMaxDetermination) = try DosingEngine.iobGreaterThanMax(
  400. iob: currentIob,
  401. maxIob: profile.maxIob,
  402. currentTemp: currentTemp,
  403. basal: basal,
  404. profile: profile,
  405. determination: determination
  406. )
  407. determination = iobGreaterThanMaxDetermination
  408. if shouldSetTempBasalForIobGreaterThanMax {
  409. return determination
  410. }
  411. // TODO: how to handle output?
  412. // TODO: how to handle logging?
  413. return determination
  414. }
  415. static func checkDeterminationInputs(
  416. glucoseStatus: GlucoseStatus?,
  417. currentTemp _: TempBasal?,
  418. iobData: [IobResult]?,
  419. profile: Profile?,
  420. trioCustomOrefVariables: TrioCustomOrefVariables,
  421. currentTime: Date = Date()
  422. ) throws {
  423. guard let glucoseStatus = glucoseStatus else {
  424. throw DeterminationError.missingGlucoseStatus
  425. }
  426. guard let profile = profile else {
  427. throw DeterminationError.missingProfile
  428. }
  429. guard profile.profileTarget(trioCustomOrefVariables: trioCustomOrefVariables) != nil else {
  430. throw DeterminationError.invalidProfileTarget
  431. }
  432. let glucoseAge = currentTime.timeIntervalSince(glucoseStatus.date)
  433. if glucoseAge > 15 * 60 {
  434. throw DeterminationError.staleGlucoseData(ageMinutes: glucoseAge / 60)
  435. }
  436. // we have to allow 38 values so that we can cancel high temps
  437. if glucoseStatus.glucose < 38 || glucoseStatus.glucose > 600 {
  438. throw DeterminationError.glucoseOutOfRange(glucose: glucoseStatus.glucose)
  439. }
  440. guard let _ = iobData else {
  441. throw DeterminationError.missingIob
  442. }
  443. }
  444. static func handleTempBasalCases(
  445. glucoseStatus: GlucoseStatus,
  446. profile: Profile,
  447. currentTemp: TempBasal?,
  448. currentTime: Date
  449. ) throws -> Determination? {
  450. let glucose = glucoseStatus.glucose
  451. let noise = glucoseStatus.noise
  452. let bgTime = glucoseStatus.date
  453. let minAgo = Decimal(currentTime.timeIntervalSince(bgTime) / 60) // minutes
  454. let shortAvgDelta = glucoseStatus.shortAvgDelta
  455. let longAvgDelta = glucoseStatus.longAvgDelta
  456. let delta = glucoseStatus.delta
  457. let device = glucoseStatus.device
  458. // Always use profile-supplied basal
  459. guard let basal = profile.currentBasal else {
  460. throw DeterminationError.missingCurrentBasal
  461. }
  462. // Compose tick for log
  463. let tick: String = (delta > -0.5) ? "+\(delta.rounded(toPlaces: 0))" : "\(delta.rounded(toPlaces: 0))"
  464. let minDelta = min(delta, shortAvgDelta)
  465. let minAvgDelta = min(shortAvgDelta, longAvgDelta)
  466. let maxDelta = max(delta, shortAvgDelta, longAvgDelta)
  467. var reason = ""
  468. // === ERROR CONDITIONS ===
  469. // xDrip code 38 = sensor error; BG <= 10 = ???/calibrating; noise >= 3 = high noise
  470. if glucose <= 10 || glucose == 38 || noise >= 3 {
  471. reason = "CGM is calibrating, in ??? state, or noise is high"
  472. }
  473. // minAgo (BG age) > 12 or < -5 = old/future BG
  474. if minAgo > 12 || minAgo < -5 {
  475. reason =
  476. "If current system time \(currentTime) is correct, then BG data is too old. The last BG data was read \(minAgo) min ago at \(bgTime)"
  477. }
  478. // CGM data unchanged (flat)
  479. if shortAvgDelta == 0 && longAvgDelta == 0 {
  480. if glucoseStatus.lastCalIndex != nil, glucoseStatus.lastCalIndex! < 3 {
  481. reason = "CGM was just calibrated"
  482. } else {
  483. reason =
  484. "CGM data is unchanged (\(glucose)+\(delta)) for 5m w/ \(shortAvgDelta) mg/dL ~15m change & \(longAvgDelta) mg/dL ~45m change"
  485. }
  486. }
  487. let errorDetected =
  488. glucose <= 10 ||
  489. glucose == 38 ||
  490. noise >= 3 ||
  491. minAgo > 12 ||
  492. minAgo < -5 ||
  493. (shortAvgDelta == 0 && longAvgDelta == 0)
  494. // === IF ERROR, CANCEL/SHORTEN TEMPS ===
  495. guard errorDetected, let currentTemp = currentTemp else { return nil }
  496. if currentTemp.rate >= basal {
  497. // Cancel high temp: set 0U/hr for 0m (neutralizes)
  498. let reasonWithAction = reason + ". Canceling high temp basal of \(currentTemp.rate)U/hr."
  499. return Determination(
  500. id: UUID(),
  501. reason: reasonWithAction,
  502. units: nil,
  503. insulinReq: nil,
  504. eventualBG: nil,
  505. sensitivityRatio: nil,
  506. rate: 0,
  507. duration: 0,
  508. iob: nil,
  509. cob: nil,
  510. predictions: nil,
  511. deliverAt: currentTime,
  512. carbsReq: nil,
  513. temp: .absolute,
  514. bg: glucose,
  515. reservoir: nil,
  516. isf: profile.sens,
  517. timestamp: currentTime,
  518. tdd: nil,
  519. current_target: profile.targetBg,
  520. minDelta: minDelta,
  521. expectedDelta: nil,
  522. minGuardBG: nil,
  523. minPredBG: nil,
  524. threshold: nil,
  525. carbRatio: profile.carbRatio,
  526. received: false
  527. )
  528. } else if currentTemp.rate == 0, currentTemp.duration > 30 {
  529. // Shorten long zero temp to 30m
  530. let reasonWithAction = reason + ". Shortening \(currentTemp.duration)m long zero temp to 30m."
  531. return Determination(
  532. id: UUID(),
  533. reason: reasonWithAction,
  534. units: nil,
  535. insulinReq: nil,
  536. eventualBG: nil,
  537. sensitivityRatio: nil,
  538. rate: 0,
  539. duration: 30,
  540. iob: nil,
  541. cob: nil,
  542. predictions: nil,
  543. deliverAt: currentTime,
  544. carbsReq: nil,
  545. temp: .absolute,
  546. bg: glucose,
  547. reservoir: nil,
  548. isf: profile.sens,
  549. timestamp: currentTime,
  550. tdd: nil,
  551. current_target: profile.targetBg,
  552. minDelta: minDelta,
  553. expectedDelta: nil,
  554. minGuardBG: nil,
  555. minPredBG: nil,
  556. threshold: nil,
  557. carbRatio: profile.carbRatio,
  558. received: false
  559. )
  560. } else {
  561. // Do nothing (temp already safe)
  562. let reasonWithAction = reason + ". Temp \(currentTemp.rate) <= current basal \(basal)U/hr; doing nothing."
  563. return Determination(
  564. id: UUID(),
  565. reason: reasonWithAction,
  566. units: nil,
  567. insulinReq: nil,
  568. eventualBG: nil,
  569. sensitivityRatio: nil,
  570. rate: currentTemp.rate,
  571. duration: Decimal(currentTemp.duration),
  572. iob: nil,
  573. cob: nil,
  574. predictions: nil,
  575. deliverAt: currentTime,
  576. carbsReq: nil,
  577. temp: currentTemp.temp,
  578. bg: glucose,
  579. reservoir: nil,
  580. isf: profile.sens,
  581. timestamp: currentTime,
  582. tdd: nil,
  583. current_target: profile.targetBg,
  584. minDelta: minDelta,
  585. expectedDelta: nil,
  586. minGuardBG: nil,
  587. minPredBG: nil,
  588. threshold: nil,
  589. carbRatio: profile.carbRatio,
  590. received: false
  591. )
  592. }
  593. }
  594. }