OpenAPS.swift 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011
  1. import Combine
  2. import CoreData
  3. import Foundation
  4. import JavaScriptCore
  5. final class OpenAPS {
  6. private let jsWorker = JavaScriptWorker()
  7. private let processQueue = DispatchQueue(label: "OpenAPS.processQueue", qos: .utility)
  8. private let storage: FileStorage
  9. private let tddStorage: TDDStorage
  10. let context = CoreDataStack.shared.newTaskContext()
  11. let jsonConverter = JSONConverter()
  12. init(storage: FileStorage, tddStorage: TDDStorage) {
  13. self.storage = storage
  14. self.tddStorage = tddStorage
  15. }
  16. static let dateFormatter: ISO8601DateFormatter = {
  17. let formatter = ISO8601DateFormatter()
  18. formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
  19. return formatter
  20. }()
  21. // Helper function to convert a Decimal? to NSDecimalNumber?
  22. func decimalToNSDecimalNumber(_ value: Decimal?) -> NSDecimalNumber? {
  23. guard let value = value else { return nil }
  24. return NSDecimalNumber(decimal: value)
  25. }
  26. // Use the helper function for cleaner code
  27. func processDetermination(_ determination: Determination) async {
  28. await context.perform {
  29. let newOrefDetermination = OrefDetermination(context: self.context)
  30. newOrefDetermination.id = UUID()
  31. newOrefDetermination.insulinSensitivity = self.decimalToNSDecimalNumber(determination.isf)
  32. newOrefDetermination.currentTarget = self.decimalToNSDecimalNumber(determination.current_target)
  33. newOrefDetermination.eventualBG = determination.eventualBG.map(NSDecimalNumber.init)
  34. newOrefDetermination.deliverAt = determination.deliverAt
  35. newOrefDetermination.insulinForManualBolus = self.decimalToNSDecimalNumber(determination.insulinForManualBolus)
  36. newOrefDetermination.carbRatio = self.decimalToNSDecimalNumber(determination.carbRatio)
  37. newOrefDetermination.glucose = self.decimalToNSDecimalNumber(determination.bg)
  38. newOrefDetermination.reservoir = self.decimalToNSDecimalNumber(determination.reservoir)
  39. newOrefDetermination.insulinReq = self.decimalToNSDecimalNumber(determination.insulinReq)
  40. newOrefDetermination.temp = determination.temp?.rawValue ?? "absolute"
  41. newOrefDetermination.rate = self.decimalToNSDecimalNumber(determination.rate)
  42. newOrefDetermination.reason = determination.reason
  43. newOrefDetermination.duration = self.decimalToNSDecimalNumber(determination.duration)
  44. newOrefDetermination.iob = self.decimalToNSDecimalNumber(determination.iob)
  45. newOrefDetermination.threshold = self.decimalToNSDecimalNumber(determination.threshold)
  46. newOrefDetermination.minDelta = self.decimalToNSDecimalNumber(determination.minDelta)
  47. newOrefDetermination.sensitivityRatio = self.decimalToNSDecimalNumber(determination.sensitivityRatio)
  48. newOrefDetermination.expectedDelta = self.decimalToNSDecimalNumber(determination.expectedDelta)
  49. newOrefDetermination.cob = Int16(Int(determination.cob ?? 0))
  50. newOrefDetermination.manualBolusErrorString = self.decimalToNSDecimalNumber(determination.manualBolusErrorString)
  51. newOrefDetermination.smbToDeliver = determination.units.map { NSDecimalNumber(decimal: $0) }
  52. newOrefDetermination.carbsRequired = Int16(Int(determination.carbsReq ?? 0))
  53. newOrefDetermination.isUploadedToNS = false
  54. if let predictions = determination.predictions {
  55. ["iob": predictions.iob, "zt": predictions.zt, "cob": predictions.cob, "uam": predictions.uam]
  56. .forEach { type, values in
  57. if let values = values {
  58. let forecast = Forecast(context: self.context)
  59. forecast.id = UUID()
  60. forecast.type = type
  61. forecast.date = Date()
  62. forecast.orefDetermination = newOrefDetermination
  63. for (index, value) in values.enumerated() {
  64. let forecastValue = ForecastValue(context: self.context)
  65. forecastValue.index = Int32(index)
  66. forecastValue.value = Int32(value)
  67. forecast.addToForecastValues(forecastValue)
  68. }
  69. newOrefDetermination.addToForecasts(forecast)
  70. }
  71. }
  72. }
  73. }
  74. // First save the current Determination to Core Data
  75. await attemptToSaveContext()
  76. }
  77. func attemptToSaveContext() async {
  78. await context.perform {
  79. do {
  80. guard self.context.hasChanges else { return }
  81. try self.context.save()
  82. } catch {
  83. debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save Determination to Core Data")
  84. }
  85. }
  86. }
  87. // fetch glucose to pass it to the meal function and to determine basal
  88. func fetchAndProcessGlucose(
  89. context: NSManagedObjectContext,
  90. shouldSmoothGlucose: Bool,
  91. fetchLimit: Int?
  92. ) async throws -> String {
  93. // make it async and await it
  94. let results = try await CoreDataStack.shared.fetchEntitiesAsync(
  95. ofType: GlucoseStored.self,
  96. onContext: context,
  97. predicate: NSPredicate.predicateForOneDayAgoInMinutes,
  98. key: "date",
  99. ascending: false,
  100. fetchLimit: fetchLimit,
  101. batchSize: 48
  102. )
  103. // mapping within the context closure, JSON conversion outside
  104. let algorithmGlucose = try await context.perform {
  105. guard let glucoseResults = results as? [GlucoseStored] else {
  106. throw CoreDataError.fetchError(function: #function, file: #file)
  107. }
  108. // extracting handler to only create it 1x
  109. let roundingBehavior = NSDecimalNumberHandler(
  110. roundingMode: .plain,
  111. scale: 0,
  112. raiseOnExactness: false,
  113. raiseOnOverflow: false,
  114. raiseOnUnderflow: false,
  115. raiseOnDivideByZero: false
  116. )
  117. return glucoseResults.map { glucose -> AlgorithmGlucose in
  118. let glucoseValue: Int16
  119. if shouldSmoothGlucose {
  120. if !glucose.isManual, let smoothedGlucose = glucose.smoothedGlucose, smoothedGlucose != 0 {
  121. glucoseValue = smoothedGlucose.rounding(accordingToBehavior: roundingBehavior).int16Value
  122. } else {
  123. // use the raw value = finger prick, so manual readings are always included for algorithm decision making
  124. // cf. https://github.com/nightscout/Trio/issues/1054
  125. glucoseValue = glucose.glucose
  126. }
  127. } else {
  128. glucoseValue = glucose.glucose
  129. }
  130. return AlgorithmGlucose(
  131. date: glucose.date,
  132. direction: glucose.direction,
  133. glucose: glucoseValue,
  134. id: glucose.id,
  135. isManual: glucose.isManual
  136. )
  137. }
  138. }
  139. return jsonConverter.convertToJSON(algorithmGlucose)
  140. }
  141. private func fetchAndProcessCarbs(additionalCarbs: Decimal? = nil, carbsDate: Date? = nil) async throws -> String {
  142. let results = try await CoreDataStack.shared.fetchEntitiesAsync(
  143. ofType: CarbEntryStored.self,
  144. onContext: context,
  145. predicate: NSPredicate.predicateForOneDayAgo,
  146. key: "date",
  147. ascending: false
  148. )
  149. let json = try await context.perform {
  150. guard let carbResults = results as? [CarbEntryStored] else {
  151. throw CoreDataError.fetchError(function: #function, file: #file)
  152. }
  153. var jsonArray = self.jsonConverter.convertToJSON(carbResults)
  154. if let additionalCarbs = additionalCarbs {
  155. let formattedDate = carbsDate.map { ISO8601DateFormatter().string(from: $0) } ?? ISO8601DateFormatter()
  156. .string(from: Date())
  157. let additionalEntry = [
  158. "carbs": Double(additionalCarbs),
  159. "actualDate": formattedDate,
  160. "id": UUID().uuidString,
  161. "note": NSNull(),
  162. "protein": 0,
  163. "created_at": formattedDate,
  164. "isFPU": false,
  165. "fat": 0,
  166. "enteredBy": "Trio"
  167. ] as [String: Any]
  168. // Assuming jsonArray is a String, convert it to a list of dictionaries first
  169. if let jsonData = jsonArray.data(using: .utf8) {
  170. var jsonList = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [[String: Any]]
  171. jsonList?.append(additionalEntry)
  172. // Convert back to JSON string
  173. if let updatedJsonData = try? JSONSerialization
  174. .data(withJSONObject: jsonList ?? [], options: .prettyPrinted)
  175. {
  176. jsonArray = String(data: updatedJsonData, encoding: .utf8) ?? jsonArray
  177. }
  178. }
  179. }
  180. return jsonArray
  181. }
  182. return json
  183. }
  184. private func fetchPumpHistoryObjectIDs() async throws -> [NSManagedObjectID]? {
  185. let results = try await CoreDataStack.shared.fetchEntitiesAsync(
  186. ofType: PumpEventStored.self,
  187. onContext: context,
  188. predicate: NSPredicate.pumpHistoryLast1440Minutes,
  189. key: "timestamp",
  190. ascending: false,
  191. batchSize: 50
  192. )
  193. return try await context.perform {
  194. guard let pumpEventResults = results as? [PumpEventStored] else {
  195. throw CoreDataError.fetchError(function: #function, file: #file)
  196. }
  197. return pumpEventResults.map(\.objectID)
  198. }
  199. }
  200. private func parsePumpHistory(
  201. _ pumpHistoryObjectIDs: [NSManagedObjectID],
  202. simulatedBolusAmount: Decimal? = nil
  203. ) async throws -> String {
  204. // Return an empty JSON object if the list of object IDs is empty
  205. guard !pumpHistoryObjectIDs.isEmpty else { return "{}" }
  206. // Addresses https://github.com/nightscout/Trio/issues/898
  207. //
  208. // On a cold start (new user, fresh onboarding, or pump disconnected > 24h),
  209. // the oldest event in pump history can be a resume with no preceding pump
  210. // activity. oref interprets this as the end of a suspend that never started,
  211. // which drives negative IOB and can cause excessive insulin delivery.
  212. let orphanedResumes = try await fetchOrphanedResumes()
  213. // Execute all operations on the background context
  214. return await context.perform {
  215. // Load and map pump events to DTOs
  216. var dtos = self.loadAndMapPumpEvents(pumpHistoryObjectIDs, orphanedResumes: orphanedResumes)
  217. // Optionally add the IOB as a DTO
  218. if let simulatedBolusAmount = simulatedBolusAmount {
  219. let simulatedBolusDTO = self.createSimulatedBolusDTO(simulatedBolusAmount: simulatedBolusAmount)
  220. dtos.insert(simulatedBolusDTO, at: 0)
  221. }
  222. // Convert the DTOs to JSON
  223. return self.jsonConverter.convertToJSON(dtos)
  224. }
  225. }
  226. private func loadAndMapPumpEvents(
  227. _ pumpHistoryObjectIDs: [NSManagedObjectID],
  228. orphanedResumes: [NSManagedObjectID]
  229. ) -> [PumpEventDTO] {
  230. OpenAPS.loadAndMapPumpEvents(pumpHistoryObjectIDs, orphanedResumes: orphanedResumes, from: context)
  231. }
  232. /// Fetches and parses pump events, expose this as static and not private for testing
  233. static func loadAndMapPumpEvents(
  234. _ pumpHistoryObjectIDs: [NSManagedObjectID],
  235. orphanedResumes: [NSManagedObjectID],
  236. from context: NSManagedObjectContext
  237. ) -> [PumpEventDTO] {
  238. let orphanedSet = Set(orphanedResumes)
  239. let filteredObjectIds = pumpHistoryObjectIDs.filter { !orphanedSet.contains($0) }
  240. // Load the pump events from the object IDs
  241. let pumpHistory: [PumpEventStored] = filteredObjectIds
  242. .compactMap { context.object(with: $0) as? PumpEventStored }
  243. // Create the DTOs
  244. let dtos: [PumpEventDTO] = pumpHistory.flatMap { event -> [PumpEventDTO] in
  245. var eventDTOs: [PumpEventDTO] = []
  246. if let bolusDTO = event.toBolusDTOEnum() {
  247. eventDTOs.append(bolusDTO)
  248. }
  249. if let tempBasalDurationDTO = event.toTempBasalDurationDTOEnum() {
  250. eventDTOs.append(tempBasalDurationDTO)
  251. }
  252. if let tempBasalDTO = event.toTempBasalDTOEnum() {
  253. eventDTOs.append(tempBasalDTO)
  254. }
  255. if let pumpSuspendDTO = event.toPumpSuspendDTO() {
  256. eventDTOs.append(pumpSuspendDTO)
  257. }
  258. if let pumpResumeDTO = event.toPumpResumeDTO() {
  259. eventDTOs.append(pumpResumeDTO)
  260. }
  261. if let rewindDTO = event.toRewindDTO() {
  262. eventDTOs.append(rewindDTO)
  263. }
  264. if let primeDTO = event.toPrimeDTO() {
  265. eventDTOs.append(primeDTO)
  266. }
  267. return eventDTOs
  268. }
  269. return dtos
  270. }
  271. private func createSimulatedBolusDTO(simulatedBolusAmount: Decimal) -> PumpEventDTO {
  272. let oneSecondAgo = Calendar.current
  273. .date(
  274. byAdding: .second,
  275. value: -1,
  276. to: Date()
  277. )! // adding -1s to the current Date ensures that oref actually uses the mock entry to calculate iob and not guard it away
  278. let dateFormatted = PumpEventStored.dateFormatter.string(from: oneSecondAgo)
  279. let bolusDTO = BolusDTO(
  280. id: UUID().uuidString,
  281. timestamp: dateFormatted,
  282. amount: Double(simulatedBolusAmount),
  283. isExternal: false,
  284. isSMB: true,
  285. duration: 0,
  286. _type: "Bolus"
  287. )
  288. return .bolus(bolusDTO)
  289. }
  290. /// Detects a cold-start orphaned resume: returns the resume's object ID if it's an orphaned resume
  291. private func fetchOrphanedResumes() async throws -> [NSManagedObjectID] {
  292. let results = try await CoreDataStack.shared.fetchEntitiesAsync(
  293. ofType: PumpEventStored.self,
  294. onContext: context,
  295. predicate: NSPredicate.pumpHistoryLast48h,
  296. key: "timestamp",
  297. ascending: true,
  298. batchSize: 250
  299. )
  300. return try await context.perform {
  301. guard let pumpEventResultsFull = results as? [PumpEventStored] else {
  302. throw CoreDataError.fetchError(function: #function, file: #file)
  303. }
  304. let pumpEventResults = pumpEventResultsFull
  305. .filter { $0.type == EventType.pumpSuspend.rawValue || $0.type == EventType.pumpResume.rawValue }
  306. // we define an orphaned resume as one without a paired suspend within
  307. // the most recent 24 hours.
  308. // **Important**: we pick 48 hours because the standard pump history
  309. // is 24 hours + 24 hours of inspection for resumes.
  310. let orphanedResumes = zip(pumpEventResults, pumpEventResults.dropFirst())
  311. .compactMap { (prev, curr) -> PumpEventStored? in
  312. guard let prevTimestamp = prev.timestamp, let currTimestamp = curr.timestamp else {
  313. return nil
  314. }
  315. let interval = currTimestamp.timeIntervalSince(prevTimestamp)
  316. // check if the current event is an orphaned resume
  317. // - previous event not a suspend
  318. // - previous event is a suspend but it's more than 24 hours ago
  319. if curr.type == EventType.pumpResume.rawValue,
  320. prev.type != EventType.pumpSuspend.rawValue || interval > TimeInterval(hours: 24)
  321. {
  322. return curr
  323. }
  324. return nil
  325. }
  326. // check the first event to see if it's an orphaned resume
  327. let firstResumeOrphaned = pumpEventResults.first.flatMap({ event -> [PumpEventStored]? in
  328. guard event.type == EventType.pumpResume.rawValue else { return nil }
  329. return [event]
  330. }) ?? []
  331. return (firstResumeOrphaned + orphanedResumes).map(\.objectID)
  332. }
  333. }
  334. func determineBasal(
  335. currentTemp: TempBasal,
  336. shouldSmoothGlucose: Bool,
  337. clock: Date = Date(),
  338. simulatedCarbsAmount: Decimal? = nil,
  339. simulatedBolusAmount: Decimal? = nil,
  340. simulatedCarbsDate: Date? = nil,
  341. simulation: Bool = false
  342. ) async throws -> Determination? {
  343. debug(.openAPS, "Start determineBasal")
  344. // temp_basal
  345. let tempBasal = currentTemp.rawJSON
  346. // Perform asynchronous calls in parallel
  347. async let pumpHistoryObjectIDs = fetchPumpHistoryObjectIDs() ?? []
  348. async let carbs = fetchAndProcessCarbs(additionalCarbs: simulatedCarbsAmount ?? 0, carbsDate: simulatedCarbsDate)
  349. async let glucose = fetchAndProcessGlucose(context: context, shouldSmoothGlucose: shouldSmoothGlucose, fetchLimit: 72)
  350. async let prepareTrioCustomOrefVariables = prepareTrioCustomOrefVariables()
  351. async let profileAsync = loadFileFromStorageAsync(name: Settings.profile)
  352. async let basalAsync = loadFileFromStorageAsync(name: Settings.basalProfile)
  353. async let autosenseAsync = loadFileFromStorageAsync(name: Settings.autosense)
  354. async let reservoirAsync = loadFileFromStorageAsync(name: Monitor.reservoir)
  355. async let preferencesAsync = storage.retrieveAsync(OpenAPS.Settings.preferences, as: Preferences.self) ?? Preferences()
  356. async let hasSufficientTddForDynamic = tddStorage.hasSufficientTDD()
  357. // Await the results of asynchronous tasks
  358. let (
  359. pumpHistoryJSON,
  360. carbsAsJSON,
  361. glucoseAsJSON,
  362. trioCustomOrefVariables,
  363. profile,
  364. basalProfile,
  365. autosens,
  366. reservoir,
  367. hasSufficientTdd
  368. ) = await (
  369. try parsePumpHistory(await pumpHistoryObjectIDs, simulatedBolusAmount: simulatedBolusAmount),
  370. try carbs,
  371. try glucose,
  372. try prepareTrioCustomOrefVariables,
  373. profileAsync,
  374. basalAsync,
  375. autosenseAsync,
  376. reservoirAsync,
  377. try hasSufficientTddForDynamic
  378. )
  379. // Meal calculation
  380. let meal = try await self.meal(
  381. pumphistory: pumpHistoryJSON,
  382. profile: profile,
  383. basalProfile: basalProfile,
  384. clock: clock,
  385. carbs: carbsAsJSON,
  386. glucose: glucoseAsJSON
  387. )
  388. // IOB calculation
  389. let iob = try await self.iob(
  390. pumphistory: pumpHistoryJSON,
  391. profile: profile,
  392. clock: clock,
  393. autosens: autosens.isEmpty ? .null : autosens
  394. )
  395. // TODO: refactor this to core data
  396. if !simulation {
  397. storage.save(iob, as: Monitor.iob)
  398. }
  399. var preferences = await preferencesAsync
  400. if !hasSufficientTdd, preferences.useNewFormula || (preferences.useNewFormula && preferences.sigmoid) {
  401. debug(.openAPS, "Insufficient TDD for dynamic formula; disabling for determine basal run.")
  402. preferences.useNewFormula = false
  403. preferences.sigmoid = false
  404. }
  405. // Determine basal
  406. let orefDetermination = try await determineBasal(
  407. glucose: glucoseAsJSON,
  408. currentTemp: tempBasal,
  409. iob: iob,
  410. profile: profile,
  411. autosens: autosens.isEmpty ? .null : autosens,
  412. meal: meal,
  413. microBolusAllowed: true,
  414. reservoir: reservoir,
  415. pumpHistory: pumpHistoryJSON,
  416. preferences: preferences,
  417. basalProfile: basalProfile,
  418. trioCustomOrefVariables: trioCustomOrefVariables
  419. )
  420. debug(.openAPS, "\(simulation ? "[SIMULATION]" : "") OREF DETERMINATION: \(orefDetermination)")
  421. if var determination = Determination(from: orefDetermination), let deliverAt = determination.deliverAt {
  422. // set both timestamp and deliverAt to the SAME date; this will be updated for timestamp once it is enacted
  423. // AAPS does it the same way! we'll follow their example!
  424. determination.timestamp = deliverAt
  425. if !simulation {
  426. // save to core data asynchronously
  427. await processDetermination(determination)
  428. }
  429. return determination
  430. } else {
  431. debug(
  432. .openAPS,
  433. "\(DebuggingIdentifiers.failed) No determination data. orefDetermination: \(orefDetermination), Determination(from: orefDetermination): \(String(describing: Determination(from: orefDetermination))), deliverAt: \(String(describing: Determination(from: orefDetermination)?.deliverAt))"
  434. )
  435. throw APSError.apsError(message: "No determination data.")
  436. }
  437. }
  438. func prepareTrioCustomOrefVariables() async throws -> RawJSON {
  439. try await context.perform {
  440. // Retrieve user preferences
  441. let userPreferences = self.storage.retrieve(OpenAPS.Settings.preferences, as: Preferences.self)
  442. let weightPercentage = userPreferences?.weightPercentage ?? 1.0
  443. let maxSMBBasalMinutes = userPreferences?.maxSMBBasalMinutes ?? 30
  444. let maxUAMBasalMinutes = userPreferences?.maxUAMSMBBasalMinutes ?? 30
  445. // Fetch historical events for Total Daily Dose (TDD) calculation
  446. let tenDaysAgo = Date().addingTimeInterval(-10.days.timeInterval)
  447. let twoHoursAgo = Date().addingTimeInterval(-2.hours.timeInterval)
  448. let historicalTDDData = try self.fetchHistoricalTDDData(from: tenDaysAgo)
  449. // Fetch the last active Override
  450. let activeOverrides = try self.fetchActiveOverrides()
  451. let isOverrideActive = activeOverrides.first?.enabled ?? false
  452. let overridePercentage = Decimal(activeOverrides.first?.percentage ?? 100)
  453. let isOverrideIndefinite = activeOverrides.first?.indefinite ?? true
  454. let disableSMBs = activeOverrides.first?.smbIsOff ?? false
  455. let overrideTargetBG = activeOverrides.first?.target?.decimalValue ?? 0
  456. // Calculate averages for Total Daily Dose (TDD)
  457. let totalTDD = historicalTDDData.compactMap { ($0["total"] as? NSDecimalNumber)?.decimalValue }.reduce(0, +)
  458. let totalDaysCount = max(historicalTDDData.count, 1)
  459. // Fetch recent TDD data for the past two hours
  460. let recentTDDData = historicalTDDData.filter { ($0["date"] as? Date ?? Date()) >= twoHoursAgo }
  461. let recentDataCount = max(recentTDDData.count, 1)
  462. let recentTotalTDD = recentTDDData.compactMap { ($0["total"] as? NSDecimalNumber)?.decimalValue }
  463. .reduce(0, +)
  464. let currentTDD = historicalTDDData.last?["total"] as? Decimal ?? 0
  465. let averageTDDLastTwoHours = recentTotalTDD / Decimal(recentDataCount)
  466. let averageTDDLastTenDays = totalTDD / Decimal(totalDaysCount)
  467. let weightedTDD = weightPercentage * averageTDDLastTwoHours + (1 - weightPercentage) * averageTDDLastTenDays
  468. let glucose = try self.fetchGlucose()
  469. // Prepare Trio's custom oref variables
  470. let trioCustomOrefVariablesData = TrioCustomOrefVariables(
  471. average_total_data: currentTDD > 0 ? averageTDDLastTenDays : 0,
  472. weightedAverage: currentTDD > 0 ? weightedTDD : 1,
  473. currentTDD: currentTDD,
  474. past2hoursAverage: currentTDD > 0 ? averageTDDLastTwoHours : 0,
  475. date: Date(),
  476. overridePercentage: overridePercentage,
  477. useOverride: isOverrideActive,
  478. duration: activeOverrides.first?.duration?.decimalValue ?? 0,
  479. unlimited: isOverrideIndefinite,
  480. overrideTarget: overrideTargetBG,
  481. smbIsOff: disableSMBs,
  482. advancedSettings: activeOverrides.first?.advancedSettings ?? false,
  483. isfAndCr: activeOverrides.first?.isfAndCr ?? false,
  484. isf: activeOverrides.first?.isf ?? false,
  485. cr: activeOverrides.first?.cr ?? false,
  486. smbIsScheduledOff: activeOverrides.first?.smbIsScheduledOff ?? false,
  487. start: (activeOverrides.first?.start ?? 0) as Decimal,
  488. end: (activeOverrides.first?.end ?? 0) as Decimal,
  489. smbMinutes: activeOverrides.first?.smbMinutes?.decimalValue ?? maxSMBBasalMinutes,
  490. uamMinutes: activeOverrides.first?.uamMinutes?.decimalValue ?? maxUAMBasalMinutes
  491. )
  492. // Save and return contents of Trio's custom oref variables
  493. self.storage.save(trioCustomOrefVariablesData, as: OpenAPS.Monitor.trio_custom_oref_variables)
  494. return self.loadFileFromStorage(name: Monitor.trio_custom_oref_variables)
  495. }
  496. }
  497. func autosense(shouldSmoothGlucose: Bool) async throws -> Autosens? {
  498. debug(.openAPS, "Start autosens")
  499. // Perform asynchronous calls in parallel
  500. async let pumpHistoryObjectIDs = fetchPumpHistoryObjectIDs() ?? []
  501. async let carbs = fetchAndProcessCarbs()
  502. async let glucose = fetchAndProcessGlucose(context: context, shouldSmoothGlucose: shouldSmoothGlucose, fetchLimit: nil)
  503. async let getProfile = loadFileFromStorageAsync(name: Settings.profile)
  504. async let getBasalProfile = loadFileFromStorageAsync(name: Settings.basalProfile)
  505. async let getTempTargets = loadFileFromStorageAsync(name: Settings.tempTargets)
  506. // Await the results of asynchronous tasks
  507. let (pumpHistoryJSON, carbsAsJSON, glucoseAsJSON, profile, basalProfile, tempTargets) = await (
  508. try parsePumpHistory(await pumpHistoryObjectIDs),
  509. try carbs,
  510. try glucose,
  511. getProfile,
  512. getBasalProfile,
  513. getTempTargets
  514. )
  515. // Autosense
  516. let autosenseResult = try await autosense(
  517. glucose: glucoseAsJSON,
  518. pumpHistory: pumpHistoryJSON,
  519. basalprofile: basalProfile,
  520. profile: profile,
  521. carbs: carbsAsJSON,
  522. temptargets: tempTargets
  523. )
  524. debug(.openAPS, "AUTOSENS: \(autosenseResult)")
  525. if var autosens = Autosens(from: autosenseResult) {
  526. autosens.timestamp = Date()
  527. await storage.saveAsync(autosens, as: Settings.autosense)
  528. return autosens
  529. } else {
  530. return nil
  531. }
  532. }
  533. func createProfiles() async throws {
  534. debug(.openAPS, "Start creating pump profile and user profile")
  535. // Load required settings and profiles asynchronously
  536. async let getPumpSettings = loadFileFromStorageAsync(name: Settings.settings)
  537. async let getBGTargets = loadFileFromStorageAsync(name: Settings.bgTargets)
  538. async let getBasalProfile = loadFileFromStorageAsync(name: Settings.basalProfile)
  539. async let getISF = loadFileFromStorageAsync(name: Settings.insulinSensitivities)
  540. async let getCR = loadFileFromStorageAsync(name: Settings.carbRatios)
  541. async let getTempTargets = loadFileFromStorageAsync(name: Settings.tempTargets)
  542. async let getModel = loadFileFromStorageAsync(name: Settings.model)
  543. async let getTrioSettingDefaults = loadFileFromStorageAsync(name: Trio.settings)
  544. let (pumpSettings, bgTargets, basalProfile, isf, cr, tempTargets, model, trioSettings) = await (
  545. getPumpSettings,
  546. getBGTargets,
  547. getBasalProfile,
  548. getISF,
  549. getCR,
  550. getTempTargets,
  551. getModel,
  552. getTrioSettingDefaults
  553. )
  554. // Retrieve user preferences, or set defaults if not available
  555. let preferences = storage.retrieve(OpenAPS.Settings.preferences, as: Preferences.self) ?? Preferences()
  556. let defaultHalfBasalTarget = preferences.halfBasalExerciseTarget
  557. var adjustedPreferences = preferences
  558. // Check for active Temp Targets and adjust HBT if necessary
  559. try await context.perform {
  560. // Check if a Temp Target is active and check HBT differs from setting and adjust
  561. if let activeTempTarget = try self.fetchActiveTempTargets().first,
  562. activeTempTarget.enabled,
  563. let targetValue = activeTempTarget.target?.decimalValue
  564. {
  565. // Compute effective HBT - handles both custom HBT and standard TT (where HBT might need adjustment)
  566. let effectiveHBT = TempTargetCalculations.computeEffectiveHBT(
  567. tempTargetHalfBasalTarget: activeTempTarget.halfBasalTarget?.decimalValue,
  568. settingHalfBasalTarget: defaultHalfBasalTarget,
  569. target: targetValue,
  570. autosensMax: preferences.autosensMax
  571. )
  572. if let effectiveHBT, effectiveHBT != defaultHalfBasalTarget {
  573. adjustedPreferences.halfBasalExerciseTarget = effectiveHBT
  574. let percentage = Int(TempTargetCalculations.computeAdjustedPercentage(
  575. halfBasalTarget: effectiveHBT,
  576. target: targetValue,
  577. autosensMax: preferences.autosensMax
  578. ))
  579. debug(
  580. .openAPS,
  581. "TempTarget: target=\(targetValue), HBT=\(defaultHalfBasalTarget), effectiveHBT=\(effectiveHBT), percentage=\(percentage)%, adjustmentType=Custom"
  582. )
  583. }
  584. }
  585. // Overwrite the lowTTlowersSens if autosensMax does not support it
  586. if preferences.lowTemptargetLowersSensitivity, preferences.autosensMax <= 1 {
  587. adjustedPreferences.lowTemptargetLowersSensitivity = false
  588. debug(.openAPS, "Setting lowTTlowersSens to false due to insufficient autosensMax: \(preferences.autosensMax)")
  589. }
  590. }
  591. do {
  592. let pumpProfile = try await makeProfile(
  593. preferences: adjustedPreferences,
  594. pumpSettings: pumpSettings,
  595. bgTargets: bgTargets,
  596. basalProfile: basalProfile,
  597. isf: isf,
  598. carbRatio: cr,
  599. tempTargets: tempTargets,
  600. model: model,
  601. autotune: RawJSON.null,
  602. trioData: trioSettings
  603. )
  604. let profile = try await makeProfile(
  605. preferences: adjustedPreferences,
  606. pumpSettings: pumpSettings,
  607. bgTargets: bgTargets,
  608. basalProfile: basalProfile,
  609. isf: isf,
  610. carbRatio: cr,
  611. tempTargets: tempTargets,
  612. model: model,
  613. autotune: RawJSON.null,
  614. trioData: trioSettings
  615. )
  616. // Save the profiles
  617. await storage.saveAsync(pumpProfile, as: Settings.pumpProfile)
  618. await storage.saveAsync(profile, as: Settings.profile)
  619. } catch {
  620. debug(
  621. .apsManager,
  622. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to create pump profile and normal profile: \(error)"
  623. )
  624. throw error
  625. }
  626. }
  627. private func iob(pumphistory: JSON, profile: JSON, clock: JSON, autosens: JSON) async throws -> RawJSON {
  628. try await withCheckedThrowingContinuation { continuation in
  629. jsWorker.inCommonContext { worker in
  630. worker.evaluateBatch(scripts: [
  631. Script(name: Prepare.log),
  632. Script(name: Bundle.iob),
  633. Script(name: Prepare.iob)
  634. ])
  635. let result = worker.call(function: Function.generate, with: [
  636. pumphistory,
  637. profile,
  638. clock,
  639. autosens
  640. ])
  641. continuation.resume(returning: result)
  642. }
  643. }
  644. }
  645. private func meal(
  646. pumphistory: JSON,
  647. profile: JSON,
  648. basalProfile: JSON,
  649. clock: JSON,
  650. carbs: JSON,
  651. glucose: JSON
  652. ) async throws -> RawJSON {
  653. try await withCheckedThrowingContinuation { continuation in
  654. jsWorker.inCommonContext { worker in
  655. worker.evaluateBatch(scripts: [
  656. Script(name: Prepare.log),
  657. Script(name: Bundle.meal),
  658. Script(name: Prepare.meal)
  659. ])
  660. let result = worker.call(function: Function.generate, with: [
  661. pumphistory,
  662. profile,
  663. clock,
  664. glucose,
  665. basalProfile,
  666. carbs
  667. ])
  668. continuation.resume(returning: result)
  669. }
  670. }
  671. }
  672. private func autosense(
  673. glucose: JSON,
  674. pumpHistory: JSON,
  675. basalprofile: JSON,
  676. profile: JSON,
  677. carbs: JSON,
  678. temptargets: JSON
  679. ) async throws -> RawJSON {
  680. try await withCheckedThrowingContinuation { continuation in
  681. jsWorker.inCommonContext { worker in
  682. worker.evaluateBatch(scripts: [
  683. Script(name: Prepare.log),
  684. Script(name: Bundle.autosens),
  685. Script(name: Prepare.autosens)
  686. ])
  687. let result = worker.call(function: Function.generate, with: [
  688. glucose,
  689. pumpHistory,
  690. basalprofile,
  691. profile,
  692. carbs,
  693. temptargets
  694. ])
  695. continuation.resume(returning: result)
  696. }
  697. }
  698. }
  699. private func determineBasal(
  700. glucose: JSON,
  701. currentTemp: JSON,
  702. iob: JSON,
  703. profile: JSON,
  704. autosens: JSON,
  705. meal: JSON,
  706. microBolusAllowed: Bool,
  707. reservoir: JSON,
  708. pumpHistory: JSON,
  709. preferences: JSON,
  710. basalProfile: JSON,
  711. trioCustomOrefVariables: JSON
  712. ) async throws -> RawJSON {
  713. try await withCheckedThrowingContinuation { continuation in
  714. jsWorker.inCommonContext { worker in
  715. worker.evaluateBatch(scripts: [
  716. Script(name: Prepare.log),
  717. Script(name: Prepare.determineBasal),
  718. Script(name: Bundle.basalSetTemp),
  719. Script(name: Bundle.getLastGlucose),
  720. Script(name: Bundle.determineBasal)
  721. ])
  722. if let middleware = self.middlewareScript(name: OpenAPS.Middleware.determineBasal) {
  723. worker.evaluate(script: middleware)
  724. }
  725. let result = worker.call(function: Function.generate, with: [
  726. iob,
  727. currentTemp,
  728. glucose,
  729. profile,
  730. autosens,
  731. meal,
  732. microBolusAllowed,
  733. reservoir,
  734. Date(),
  735. pumpHistory,
  736. preferences,
  737. basalProfile,
  738. trioCustomOrefVariables
  739. ])
  740. continuation.resume(returning: result)
  741. }
  742. }
  743. }
  744. private func exportDefaultPreferences() -> RawJSON {
  745. dispatchPrecondition(condition: .onQueue(processQueue))
  746. return jsWorker.inCommonContext { worker in
  747. worker.evaluateBatch(scripts: [
  748. Script(name: Prepare.log),
  749. Script(name: Bundle.profile),
  750. Script(name: Prepare.profile)
  751. ])
  752. return worker.call(function: Function.exportDefaults, with: [])
  753. }
  754. }
  755. private func makeProfile(
  756. preferences: JSON,
  757. pumpSettings: JSON,
  758. bgTargets: JSON,
  759. basalProfile: JSON,
  760. isf: JSON,
  761. carbRatio: JSON,
  762. tempTargets: JSON,
  763. model: JSON,
  764. autotune: JSON,
  765. trioData: JSON
  766. ) async throws -> RawJSON {
  767. try await withCheckedThrowingContinuation { continuation in
  768. jsWorker.inCommonContext { worker in
  769. worker.evaluateBatch(scripts: [
  770. Script(name: Prepare.log),
  771. Script(name: Bundle.profile),
  772. Script(name: Prepare.profile)
  773. ])
  774. let result = worker.call(function: Function.generate, with: [
  775. pumpSettings,
  776. bgTargets,
  777. isf,
  778. basalProfile,
  779. preferences,
  780. carbRatio,
  781. tempTargets,
  782. model,
  783. autotune,
  784. trioData
  785. ])
  786. continuation.resume(returning: result)
  787. }
  788. }
  789. }
  790. private func loadJSON(name: String) -> String {
  791. try! String(contentsOf: Foundation.Bundle.main.url(forResource: "json/\(name)", withExtension: "json")!)
  792. }
  793. private func loadFileFromStorage(name: String) -> RawJSON {
  794. storage.retrieveRaw(name) ?? OpenAPS.defaults(for: name)
  795. }
  796. private func loadFileFromStorageAsync(name: String) async -> RawJSON {
  797. await withCheckedContinuation { continuation in
  798. DispatchQueue.global(qos: .userInitiated).async {
  799. let result = self.storage.retrieveRaw(name) ?? OpenAPS.defaults(for: name)
  800. continuation.resume(returning: result)
  801. }
  802. }
  803. }
  804. private func middlewareScript(name: String) -> Script? {
  805. if let body = storage.retrieveRaw(name) {
  806. return Script(name: name, body: body)
  807. }
  808. if let url = Foundation.Bundle.main.url(forResource: "javascript/\(name)", withExtension: "") {
  809. do {
  810. let body = try String(contentsOf: url)
  811. return Script(name: name, body: body)
  812. } catch {
  813. debug(.openAPS, "Failed to load script \(name): \(error)")
  814. }
  815. }
  816. return nil
  817. }
  818. static func defaults(for file: String) -> RawJSON {
  819. let prefix = file.hasSuffix(".json") ? "json/defaults" : "javascript"
  820. guard let url = Foundation.Bundle.main.url(forResource: "\(prefix)/\(file)", withExtension: "") else {
  821. return ""
  822. }
  823. return (try? String(contentsOf: url)) ?? ""
  824. }
  825. func processAndSave(forecastData: [String: [Int]]) {
  826. let currentDate = Date()
  827. context.perform {
  828. for (type, values) in forecastData {
  829. self.createForecast(type: type, values: values, date: currentDate, context: self.context)
  830. }
  831. do {
  832. guard self.context.hasChanges else { return }
  833. try self.context.save()
  834. } catch {
  835. print(error.localizedDescription)
  836. }
  837. }
  838. }
  839. func createForecast(type: String, values: [Int], date: Date, context: NSManagedObjectContext) {
  840. let forecast = Forecast(context: context)
  841. forecast.id = UUID()
  842. forecast.date = date
  843. forecast.type = type
  844. for (index, value) in values.enumerated() {
  845. let forecastValue = ForecastValue(context: context)
  846. forecastValue.value = Int32(value)
  847. forecastValue.index = Int32(index)
  848. forecastValue.forecast = forecast
  849. }
  850. }
  851. }
  852. // Non-Async fetch methods for trio_custom_oref_variables
  853. extension OpenAPS {
  854. func fetchActiveTempTargets() throws -> [TempTargetStored] {
  855. try CoreDataStack.shared.fetchEntities(
  856. ofType: TempTargetStored.self,
  857. onContext: context,
  858. predicate: NSPredicate.lastActiveTempTarget,
  859. key: "date",
  860. ascending: false,
  861. fetchLimit: 1
  862. ) as? [TempTargetStored] ?? []
  863. }
  864. func fetchActiveOverrides() throws -> [OverrideStored] {
  865. try CoreDataStack.shared.fetchEntities(
  866. ofType: OverrideStored.self,
  867. onContext: context,
  868. predicate: NSPredicate.lastActiveOverride,
  869. key: "date",
  870. ascending: false,
  871. fetchLimit: 1
  872. ) as? [OverrideStored] ?? []
  873. }
  874. func fetchHistoricalTDDData(from date: Date) throws -> [[String: Any]] {
  875. try CoreDataStack.shared.fetchEntities(
  876. ofType: TDDStored.self,
  877. onContext: context,
  878. predicate: NSPredicate(format: "date > %@ AND total > 0", date as NSDate),
  879. key: "date",
  880. ascending: true,
  881. propertiesToFetch: ["date", "total"]
  882. ) as? [[String: Any]] ?? []
  883. }
  884. func fetchGlucose() throws -> [GlucoseStored] {
  885. let results = try CoreDataStack.shared.fetchEntities(
  886. ofType: GlucoseStored.self,
  887. onContext: context,
  888. predicate: NSPredicate.predicateFor30MinAgo,
  889. key: "date",
  890. ascending: false,
  891. fetchLimit: 4
  892. )
  893. return try context.perform {
  894. guard let glucoseResults = results as? [GlucoseStored] else {
  895. throw CoreDataError.fetchError(function: #function, file: #file)
  896. }
  897. return glucoseResults
  898. }
  899. }
  900. }