DetermineBasalGenerator.swift 27 KB

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