OpenAPS.swift 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893
  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. private func fetchAndProcessGlucose(fetchLimit: Int?) async throws -> String {
  89. let results = try await CoreDataStack.shared.fetchEntitiesAsync(
  90. ofType: GlucoseStored.self,
  91. onContext: context,
  92. predicate: NSPredicate.predicateForOneDayAgoInMinutes,
  93. key: "date",
  94. ascending: false,
  95. fetchLimit: fetchLimit,
  96. batchSize: 48
  97. )
  98. return try await context.perform {
  99. guard let glucoseResults = results as? [GlucoseStored] else {
  100. throw CoreDataError.fetchError(function: #function, file: #file)
  101. }
  102. // convert to JSON
  103. return self.jsonConverter.convertToJSON(glucoseResults)
  104. }
  105. }
  106. private func fetchAndProcessCarbs(additionalCarbs: Decimal? = nil, carbsDate: Date? = nil) async throws -> String {
  107. let results = try await CoreDataStack.shared.fetchEntitiesAsync(
  108. ofType: CarbEntryStored.self,
  109. onContext: context,
  110. predicate: NSPredicate.predicateForOneDayAgo,
  111. key: "date",
  112. ascending: false
  113. )
  114. let json = try await context.perform {
  115. guard let carbResults = results as? [CarbEntryStored] else {
  116. throw CoreDataError.fetchError(function: #function, file: #file)
  117. }
  118. var jsonArray = self.jsonConverter.convertToJSON(carbResults)
  119. if let additionalCarbs = additionalCarbs {
  120. let formattedDate = carbsDate.map { ISO8601DateFormatter().string(from: $0) } ?? ISO8601DateFormatter()
  121. .string(from: Date())
  122. let additionalEntry = [
  123. "carbs": Double(additionalCarbs),
  124. "actualDate": formattedDate,
  125. "id": UUID().uuidString,
  126. "note": NSNull(),
  127. "protein": 0,
  128. "created_at": formattedDate,
  129. "isFPU": false,
  130. "fat": 0,
  131. "enteredBy": "Trio"
  132. ] as [String: Any]
  133. // Assuming jsonArray is a String, convert it to a list of dictionaries first
  134. if let jsonData = jsonArray.data(using: .utf8) {
  135. var jsonList = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [[String: Any]]
  136. jsonList?.append(additionalEntry)
  137. // Convert back to JSON string
  138. if let updatedJsonData = try? JSONSerialization
  139. .data(withJSONObject: jsonList ?? [], options: .prettyPrinted)
  140. {
  141. jsonArray = String(data: updatedJsonData, encoding: .utf8) ?? jsonArray
  142. }
  143. }
  144. }
  145. return jsonArray
  146. }
  147. return json
  148. }
  149. private func fetchPumpHistoryObjectIDs() async throws -> [NSManagedObjectID]? {
  150. let results = try await CoreDataStack.shared.fetchEntitiesAsync(
  151. ofType: PumpEventStored.self,
  152. onContext: context,
  153. predicate: NSPredicate.pumpHistoryLast1440Minutes,
  154. key: "timestamp",
  155. ascending: false,
  156. batchSize: 50
  157. )
  158. return try await context.perform {
  159. guard let pumpEventResults = results as? [PumpEventStored] else {
  160. throw CoreDataError.fetchError(function: #function, file: #file)
  161. }
  162. return pumpEventResults.map(\.objectID)
  163. }
  164. }
  165. private func parsePumpHistory(
  166. _ pumpHistoryObjectIDs: [NSManagedObjectID],
  167. simulatedBolusAmount: Decimal? = nil
  168. ) async -> String {
  169. // Return an empty JSON object if the list of object IDs is empty
  170. guard !pumpHistoryObjectIDs.isEmpty else { return "{}" }
  171. // Execute all operations on the background context
  172. return await context.perform {
  173. // Load and map pump events to DTOs
  174. var dtos = self.loadAndMapPumpEvents(pumpHistoryObjectIDs)
  175. // Optionally add the IOB as a DTO
  176. if let simulatedBolusAmount = simulatedBolusAmount {
  177. let simulatedBolusDTO = self.createSimulatedBolusDTO(simulatedBolusAmount: simulatedBolusAmount)
  178. dtos.insert(simulatedBolusDTO, at: 0)
  179. }
  180. // Convert the DTOs to JSON
  181. return self.jsonConverter.convertToJSON(dtos)
  182. }
  183. }
  184. private func loadAndMapPumpEvents(_ pumpHistoryObjectIDs: [NSManagedObjectID]) -> [PumpEventDTO] {
  185. OpenAPS.loadAndMapPumpEvents(pumpHistoryObjectIDs, from: context)
  186. }
  187. /// Fetches and parses pump events, expose this as static and not private for testing
  188. static func loadAndMapPumpEvents(
  189. _ pumpHistoryObjectIDs: [NSManagedObjectID],
  190. from context: NSManagedObjectContext
  191. ) -> [PumpEventDTO] {
  192. // Load the pump events from the object IDs
  193. let pumpHistory: [PumpEventStored] = pumpHistoryObjectIDs
  194. .compactMap { context.object(with: $0) as? PumpEventStored }
  195. // Create the DTOs
  196. let dtos: [PumpEventDTO] = pumpHistory.flatMap { event -> [PumpEventDTO] in
  197. var eventDTOs: [PumpEventDTO] = []
  198. if let bolusDTO = event.toBolusDTOEnum() {
  199. eventDTOs.append(bolusDTO)
  200. }
  201. if let tempBasalDurationDTO = event.toTempBasalDurationDTOEnum() {
  202. eventDTOs.append(tempBasalDurationDTO)
  203. }
  204. if let tempBasalDTO = event.toTempBasalDTOEnum() {
  205. eventDTOs.append(tempBasalDTO)
  206. }
  207. if let pumpSuspendDTO = event.toPumpSuspendDTO() {
  208. eventDTOs.append(pumpSuspendDTO)
  209. }
  210. if let pumpResumeDTO = event.toPumpResumeDTO() {
  211. eventDTOs.append(pumpResumeDTO)
  212. }
  213. if let rewindDTO = event.toRewindDTO() {
  214. eventDTOs.append(rewindDTO)
  215. }
  216. if let primeDTO = event.toPrimeDTO() {
  217. eventDTOs.append(primeDTO)
  218. }
  219. return eventDTOs
  220. }
  221. return dtos
  222. }
  223. private func createSimulatedBolusDTO(simulatedBolusAmount: Decimal) -> PumpEventDTO {
  224. let oneSecondAgo = Calendar.current
  225. .date(
  226. byAdding: .second,
  227. value: -1,
  228. to: Date()
  229. )! // adding -1s to the current Date ensures that oref actually uses the mock entry to calculate iob and not guard it away
  230. let dateFormatted = PumpEventStored.dateFormatter.string(from: oneSecondAgo)
  231. let bolusDTO = BolusDTO(
  232. id: UUID().uuidString,
  233. timestamp: dateFormatted,
  234. amount: Double(simulatedBolusAmount),
  235. isExternal: false,
  236. isSMB: true,
  237. duration: 0,
  238. _type: "Bolus"
  239. )
  240. return .bolus(bolusDTO)
  241. }
  242. func determineBasal(
  243. currentTemp: TempBasal,
  244. clock: Date = Date(),
  245. simulatedCarbsAmount: Decimal? = nil,
  246. simulatedBolusAmount: Decimal? = nil,
  247. simulatedCarbsDate: Date? = nil,
  248. simulation: Bool = false
  249. ) async throws -> Determination? {
  250. debug(.openAPS, "Start determineBasal")
  251. // temp_basal
  252. let tempBasal = currentTemp.rawJSON
  253. // Perform asynchronous calls in parallel
  254. async let pumpHistoryObjectIDs = fetchPumpHistoryObjectIDs() ?? []
  255. async let carbs = fetchAndProcessCarbs(additionalCarbs: simulatedCarbsAmount ?? 0, carbsDate: simulatedCarbsDate)
  256. async let glucose = fetchAndProcessGlucose(fetchLimit: 72)
  257. async let prepareTrioCustomOrefVariables = prepareTrioCustomOrefVariables()
  258. async let profileAsync = loadFileFromStorageAsync(name: Settings.profile)
  259. async let basalAsync = loadFileFromStorageAsync(name: Settings.basalProfile)
  260. async let autosenseAsync = loadFileFromStorageAsync(name: Settings.autosense)
  261. async let reservoirAsync = loadFileFromStorageAsync(name: Monitor.reservoir)
  262. async let preferencesAsync = storage.retrieveAsync(OpenAPS.Settings.preferences, as: Preferences.self) ?? Preferences()
  263. async let hasSufficientTddForDynamic = tddStorage.hasSufficientTDD()
  264. // Await the results of asynchronous tasks
  265. let (
  266. pumpHistoryJSON,
  267. carbsAsJSON,
  268. glucoseAsJSON,
  269. trioCustomOrefVariables,
  270. profile,
  271. basalProfile,
  272. autosens,
  273. reservoir,
  274. hasSufficientTdd
  275. ) = await (
  276. try parsePumpHistory(await pumpHistoryObjectIDs, simulatedBolusAmount: simulatedBolusAmount),
  277. try carbs,
  278. try glucose,
  279. try prepareTrioCustomOrefVariables,
  280. profileAsync,
  281. basalAsync,
  282. autosenseAsync,
  283. reservoirAsync,
  284. try hasSufficientTddForDynamic
  285. )
  286. // Meal calculation
  287. let meal = try await self.meal(
  288. pumphistory: pumpHistoryJSON,
  289. profile: profile,
  290. basalProfile: basalProfile,
  291. clock: clock,
  292. carbs: carbsAsJSON,
  293. glucose: glucoseAsJSON
  294. )
  295. // IOB calculation
  296. let iob = try await self.iob(
  297. pumphistory: pumpHistoryJSON,
  298. profile: profile,
  299. clock: clock,
  300. autosens: autosens.isEmpty ? .null : autosens
  301. )
  302. // TODO: refactor this to core data
  303. if !simulation {
  304. storage.save(iob, as: Monitor.iob)
  305. }
  306. var preferences = await preferencesAsync
  307. if !hasSufficientTdd, preferences.useNewFormula || (preferences.useNewFormula && preferences.sigmoid) {
  308. debug(.openAPS, "Insufficient TDD for dynamic formula; disabling for determine basal run.")
  309. preferences.useNewFormula = false
  310. preferences.sigmoid = false
  311. }
  312. // Determine basal
  313. let orefDetermination = try await determineBasal(
  314. glucose: glucoseAsJSON,
  315. currentTemp: tempBasal,
  316. iob: iob,
  317. profile: profile,
  318. autosens: autosens.isEmpty ? .null : autosens,
  319. meal: meal,
  320. microBolusAllowed: true,
  321. reservoir: reservoir,
  322. pumpHistory: pumpHistoryJSON,
  323. preferences: preferences,
  324. basalProfile: basalProfile,
  325. trioCustomOrefVariables: trioCustomOrefVariables
  326. )
  327. debug(.openAPS, "\(simulation ? "[SIMULATION]" : "") OREF DETERMINATION: \(orefDetermination)")
  328. if var determination = Determination(from: orefDetermination), let deliverAt = determination.deliverAt {
  329. // set both timestamp and deliverAt to the SAME date; this will be updated for timestamp once it is enacted
  330. // AAPS does it the same way! we'll follow their example!
  331. determination.timestamp = deliverAt
  332. if !simulation {
  333. // save to core data asynchronously
  334. await processDetermination(determination)
  335. }
  336. return determination
  337. } else {
  338. debug(
  339. .openAPS,
  340. "\(DebuggingIdentifiers.failed) No determination data. orefDetermination: \(orefDetermination), Determination(from: orefDetermination): \(String(describing: Determination(from: orefDetermination))), deliverAt: \(String(describing: Determination(from: orefDetermination)?.deliverAt))"
  341. )
  342. throw APSError.apsError(message: "No determination data.")
  343. }
  344. }
  345. func prepareTrioCustomOrefVariables() async throws -> RawJSON {
  346. try await context.perform {
  347. // Retrieve user preferences
  348. let userPreferences = self.storage.retrieve(OpenAPS.Settings.preferences, as: Preferences.self)
  349. let weightPercentage = userPreferences?.weightPercentage ?? 1.0
  350. let maxSMBBasalMinutes = userPreferences?.maxSMBBasalMinutes ?? 30
  351. let maxUAMBasalMinutes = userPreferences?.maxUAMSMBBasalMinutes ?? 30
  352. // Fetch historical events for Total Daily Dose (TDD) calculation
  353. let tenDaysAgo = Date().addingTimeInterval(-10.days.timeInterval)
  354. let twoHoursAgo = Date().addingTimeInterval(-2.hours.timeInterval)
  355. let historicalTDDData = try self.fetchHistoricalTDDData(from: tenDaysAgo)
  356. // Fetch the last active Override
  357. let activeOverrides = try self.fetchActiveOverrides()
  358. let isOverrideActive = activeOverrides.first?.enabled ?? false
  359. let overridePercentage = Decimal(activeOverrides.first?.percentage ?? 100)
  360. let isOverrideIndefinite = activeOverrides.first?.indefinite ?? true
  361. let disableSMBs = activeOverrides.first?.smbIsOff ?? false
  362. let overrideTargetBG = activeOverrides.first?.target?.decimalValue ?? 0
  363. // Calculate averages for Total Daily Dose (TDD)
  364. let totalTDD = historicalTDDData.compactMap { ($0["total"] as? NSDecimalNumber)?.decimalValue }.reduce(0, +)
  365. let totalDaysCount = max(historicalTDDData.count, 1)
  366. // Fetch recent TDD data for the past two hours
  367. let recentTDDData = historicalTDDData.filter { ($0["date"] as? Date ?? Date()) >= twoHoursAgo }
  368. let recentDataCount = max(recentTDDData.count, 1)
  369. let recentTotalTDD = recentTDDData.compactMap { ($0["total"] as? NSDecimalNumber)?.decimalValue }
  370. .reduce(0, +)
  371. let currentTDD = historicalTDDData.last?["total"] as? Decimal ?? 0
  372. let averageTDDLastTwoHours = recentTotalTDD / Decimal(recentDataCount)
  373. let averageTDDLastTenDays = totalTDD / Decimal(totalDaysCount)
  374. let weightedTDD = weightPercentage * averageTDDLastTwoHours + (1 - weightPercentage) * averageTDDLastTenDays
  375. let glucose = try self.fetchGlucose()
  376. // Prepare Trio's custom oref variables
  377. let trioCustomOrefVariablesData = TrioCustomOrefVariables(
  378. average_total_data: currentTDD > 0 ? averageTDDLastTenDays : 0,
  379. weightedAverage: currentTDD > 0 ? weightedTDD : 1,
  380. currentTDD: currentTDD,
  381. past2hoursAverage: currentTDD > 0 ? averageTDDLastTwoHours : 0,
  382. date: Date(),
  383. overridePercentage: overridePercentage,
  384. useOverride: isOverrideActive,
  385. duration: activeOverrides.first?.duration?.decimalValue ?? 0,
  386. unlimited: isOverrideIndefinite,
  387. overrideTarget: overrideTargetBG,
  388. smbIsOff: disableSMBs,
  389. advancedSettings: activeOverrides.first?.advancedSettings ?? false,
  390. isfAndCr: activeOverrides.first?.isfAndCr ?? false,
  391. isf: activeOverrides.first?.isf ?? false,
  392. cr: activeOverrides.first?.cr ?? false,
  393. smbIsScheduledOff: activeOverrides.first?.smbIsScheduledOff ?? false,
  394. start: (activeOverrides.first?.start ?? 0) as Decimal,
  395. end: (activeOverrides.first?.end ?? 0) as Decimal,
  396. smbMinutes: activeOverrides.first?.smbMinutes?.decimalValue ?? maxSMBBasalMinutes,
  397. uamMinutes: activeOverrides.first?.uamMinutes?.decimalValue ?? maxUAMBasalMinutes
  398. )
  399. // Save and return contents of Trio's custom oref variables
  400. self.storage.save(trioCustomOrefVariablesData, as: OpenAPS.Monitor.trio_custom_oref_variables)
  401. return self.loadFileFromStorage(name: Monitor.trio_custom_oref_variables)
  402. }
  403. }
  404. func autosense() async throws -> Autosens? {
  405. debug(.openAPS, "Start autosens")
  406. // Perform asynchronous calls in parallel
  407. async let pumpHistoryObjectIDs = fetchPumpHistoryObjectIDs() ?? []
  408. async let carbs = fetchAndProcessCarbs()
  409. async let glucose = fetchAndProcessGlucose(fetchLimit: nil)
  410. async let getProfile = loadFileFromStorageAsync(name: Settings.profile)
  411. async let getBasalProfile = loadFileFromStorageAsync(name: Settings.basalProfile)
  412. async let getTempTargets = loadFileFromStorageAsync(name: Settings.tempTargets)
  413. // Await the results of asynchronous tasks
  414. let (pumpHistoryJSON, carbsAsJSON, glucoseAsJSON, profile, basalProfile, tempTargets) = await (
  415. try parsePumpHistory(await pumpHistoryObjectIDs),
  416. try carbs,
  417. try glucose,
  418. getProfile,
  419. getBasalProfile,
  420. getTempTargets
  421. )
  422. // Autosense
  423. let autosenseResult = try await autosense(
  424. glucose: glucoseAsJSON,
  425. pumpHistory: pumpHistoryJSON,
  426. basalprofile: basalProfile,
  427. profile: profile,
  428. carbs: carbsAsJSON,
  429. temptargets: tempTargets
  430. )
  431. debug(.openAPS, "AUTOSENS: \(autosenseResult)")
  432. if var autosens = Autosens(from: autosenseResult) {
  433. autosens.timestamp = Date()
  434. await storage.saveAsync(autosens, as: Settings.autosense)
  435. return autosens
  436. } else {
  437. return nil
  438. }
  439. }
  440. func createProfiles() async throws {
  441. debug(.openAPS, "Start creating pump profile and user profile")
  442. // Load required settings and profiles asynchronously
  443. async let getPumpSettings = loadFileFromStorageAsync(name: Settings.settings)
  444. async let getBGTargets = loadFileFromStorageAsync(name: Settings.bgTargets)
  445. async let getBasalProfile = loadFileFromStorageAsync(name: Settings.basalProfile)
  446. async let getISF = loadFileFromStorageAsync(name: Settings.insulinSensitivities)
  447. async let getCR = loadFileFromStorageAsync(name: Settings.carbRatios)
  448. async let getTempTargets = loadFileFromStorageAsync(name: Settings.tempTargets)
  449. async let getModel = loadFileFromStorageAsync(name: Settings.model)
  450. async let getTrioSettingDefaults = loadFileFromStorageAsync(name: Trio.settings)
  451. let (pumpSettings, bgTargets, basalProfile, isf, cr, tempTargets, model, trioSettings) = await (
  452. getPumpSettings,
  453. getBGTargets,
  454. getBasalProfile,
  455. getISF,
  456. getCR,
  457. getTempTargets,
  458. getModel,
  459. getTrioSettingDefaults
  460. )
  461. // Retrieve user preferences, or set defaults if not available
  462. let preferences = storage.retrieve(OpenAPS.Settings.preferences, as: Preferences.self) ?? Preferences()
  463. let defaultHalfBasalTarget = preferences.halfBasalExerciseTarget
  464. var adjustedPreferences = preferences
  465. // Check for active Temp Targets and adjust HBT if necessary
  466. try await context.perform {
  467. // Check if a Temp Target is active and if its HBT differs from user preferences
  468. if let activeTempTarget = try self.fetchActiveTempTargets().first,
  469. activeTempTarget.enabled,
  470. let activeHBT = activeTempTarget.halfBasalTarget?.decimalValue,
  471. activeHBT != defaultHalfBasalTarget
  472. {
  473. // Overwrite the HBT in preferences
  474. adjustedPreferences.halfBasalExerciseTarget = activeHBT
  475. debug(.openAPS, "Updated halfBasalExerciseTarget to active Temp Target value: \(activeHBT)")
  476. }
  477. // Overwrite the lowTTlowersSens if autosensMax does not support it
  478. if preferences.lowTemptargetLowersSensitivity, preferences.autosensMax <= 1 {
  479. adjustedPreferences.lowTemptargetLowersSensitivity = false
  480. debug(.openAPS, "Setting lowTTlowersSens to false due to insufficient autosensMax: \(preferences.autosensMax)")
  481. }
  482. }
  483. do {
  484. let pumpProfile = try await makeProfile(
  485. preferences: adjustedPreferences,
  486. pumpSettings: pumpSettings,
  487. bgTargets: bgTargets,
  488. basalProfile: basalProfile,
  489. isf: isf,
  490. carbRatio: cr,
  491. tempTargets: tempTargets,
  492. model: model,
  493. autotune: RawJSON.null,
  494. trioData: trioSettings
  495. )
  496. let profile = try await makeProfile(
  497. preferences: adjustedPreferences,
  498. pumpSettings: pumpSettings,
  499. bgTargets: bgTargets,
  500. basalProfile: basalProfile,
  501. isf: isf,
  502. carbRatio: cr,
  503. tempTargets: tempTargets,
  504. model: model,
  505. autotune: RawJSON.null,
  506. trioData: trioSettings
  507. )
  508. // Save the profiles
  509. await storage.saveAsync(pumpProfile, as: Settings.pumpProfile)
  510. await storage.saveAsync(profile, as: Settings.profile)
  511. } catch {
  512. debug(
  513. .apsManager,
  514. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to create pump profile and normal profile: \(error)"
  515. )
  516. throw error
  517. }
  518. }
  519. private func iob(pumphistory: JSON, profile: JSON, clock: JSON, autosens: JSON) async throws -> RawJSON {
  520. try await withCheckedThrowingContinuation { continuation in
  521. jsWorker.inCommonContext { worker in
  522. worker.evaluateBatch(scripts: [
  523. Script(name: Prepare.log),
  524. Script(name: Bundle.iob),
  525. Script(name: Prepare.iob)
  526. ])
  527. let result = worker.call(function: Function.generate, with: [
  528. pumphistory,
  529. profile,
  530. clock,
  531. autosens
  532. ])
  533. continuation.resume(returning: result)
  534. }
  535. }
  536. }
  537. private func meal(
  538. pumphistory: JSON,
  539. profile: JSON,
  540. basalProfile: JSON,
  541. clock: JSON,
  542. carbs: JSON,
  543. glucose: JSON
  544. ) async throws -> RawJSON {
  545. try await withCheckedThrowingContinuation { continuation in
  546. jsWorker.inCommonContext { worker in
  547. worker.evaluateBatch(scripts: [
  548. Script(name: Prepare.log),
  549. Script(name: Bundle.meal),
  550. Script(name: Prepare.meal)
  551. ])
  552. let result = worker.call(function: Function.generate, with: [
  553. pumphistory,
  554. profile,
  555. clock,
  556. glucose,
  557. basalProfile,
  558. carbs
  559. ])
  560. continuation.resume(returning: result)
  561. }
  562. }
  563. }
  564. private func autosense(
  565. glucose: JSON,
  566. pumpHistory: JSON,
  567. basalprofile: JSON,
  568. profile: JSON,
  569. carbs: JSON,
  570. temptargets: JSON
  571. ) async throws -> RawJSON {
  572. try await withCheckedThrowingContinuation { continuation in
  573. jsWorker.inCommonContext { worker in
  574. worker.evaluateBatch(scripts: [
  575. Script(name: Prepare.log),
  576. Script(name: Bundle.autosens),
  577. Script(name: Prepare.autosens)
  578. ])
  579. let result = worker.call(function: Function.generate, with: [
  580. glucose,
  581. pumpHistory,
  582. basalprofile,
  583. profile,
  584. carbs,
  585. temptargets
  586. ])
  587. continuation.resume(returning: result)
  588. }
  589. }
  590. }
  591. private func determineBasal(
  592. glucose: JSON,
  593. currentTemp: JSON,
  594. iob: JSON,
  595. profile: JSON,
  596. autosens: JSON,
  597. meal: JSON,
  598. microBolusAllowed: Bool,
  599. reservoir: JSON,
  600. pumpHistory: JSON,
  601. preferences: JSON,
  602. basalProfile: JSON,
  603. trioCustomOrefVariables: JSON
  604. ) async throws -> RawJSON {
  605. try await withCheckedThrowingContinuation { continuation in
  606. jsWorker.inCommonContext { worker in
  607. worker.evaluateBatch(scripts: [
  608. Script(name: Prepare.log),
  609. Script(name: Prepare.determineBasal),
  610. Script(name: Bundle.basalSetTemp),
  611. Script(name: Bundle.getLastGlucose),
  612. Script(name: Bundle.determineBasal)
  613. ])
  614. if let middleware = self.middlewareScript(name: OpenAPS.Middleware.determineBasal) {
  615. worker.evaluate(script: middleware)
  616. }
  617. let result = worker.call(function: Function.generate, with: [
  618. iob,
  619. currentTemp,
  620. glucose,
  621. profile,
  622. autosens,
  623. meal,
  624. microBolusAllowed,
  625. reservoir,
  626. Date(),
  627. pumpHistory,
  628. preferences,
  629. basalProfile,
  630. trioCustomOrefVariables
  631. ])
  632. continuation.resume(returning: result)
  633. }
  634. }
  635. }
  636. private func exportDefaultPreferences() -> RawJSON {
  637. dispatchPrecondition(condition: .onQueue(processQueue))
  638. return jsWorker.inCommonContext { worker in
  639. worker.evaluateBatch(scripts: [
  640. Script(name: Prepare.log),
  641. Script(name: Bundle.profile),
  642. Script(name: Prepare.profile)
  643. ])
  644. return worker.call(function: Function.exportDefaults, with: [])
  645. }
  646. }
  647. private func makeProfile(
  648. preferences: JSON,
  649. pumpSettings: JSON,
  650. bgTargets: JSON,
  651. basalProfile: JSON,
  652. isf: JSON,
  653. carbRatio: JSON,
  654. tempTargets: JSON,
  655. model: JSON,
  656. autotune: JSON,
  657. trioData: JSON
  658. ) async throws -> RawJSON {
  659. try await withCheckedThrowingContinuation { continuation in
  660. jsWorker.inCommonContext { worker in
  661. worker.evaluateBatch(scripts: [
  662. Script(name: Prepare.log),
  663. Script(name: Bundle.profile),
  664. Script(name: Prepare.profile)
  665. ])
  666. let result = worker.call(function: Function.generate, with: [
  667. pumpSettings,
  668. bgTargets,
  669. isf,
  670. basalProfile,
  671. preferences,
  672. carbRatio,
  673. tempTargets,
  674. model,
  675. autotune,
  676. trioData
  677. ])
  678. continuation.resume(returning: result)
  679. }
  680. }
  681. }
  682. private func loadJSON(name: String) -> String {
  683. try! String(contentsOf: Foundation.Bundle.main.url(forResource: "json/\(name)", withExtension: "json")!)
  684. }
  685. private func loadFileFromStorage(name: String) -> RawJSON {
  686. storage.retrieveRaw(name) ?? OpenAPS.defaults(for: name)
  687. }
  688. private func loadFileFromStorageAsync(name: String) async -> RawJSON {
  689. await withCheckedContinuation { continuation in
  690. DispatchQueue.global(qos: .userInitiated).async {
  691. let result = self.storage.retrieveRaw(name) ?? OpenAPS.defaults(for: name)
  692. continuation.resume(returning: result)
  693. }
  694. }
  695. }
  696. private func middlewareScript(name: String) -> Script? {
  697. if let body = storage.retrieveRaw(name) {
  698. return Script(name: name, body: body)
  699. }
  700. if let url = Foundation.Bundle.main.url(forResource: "javascript/\(name)", withExtension: "") {
  701. do {
  702. let body = try String(contentsOf: url)
  703. return Script(name: name, body: body)
  704. } catch {
  705. debug(.openAPS, "Failed to load script \(name): \(error)")
  706. }
  707. }
  708. return nil
  709. }
  710. static func defaults(for file: String) -> RawJSON {
  711. let prefix = file.hasSuffix(".json") ? "json/defaults" : "javascript"
  712. guard let url = Foundation.Bundle.main.url(forResource: "\(prefix)/\(file)", withExtension: "") else {
  713. return ""
  714. }
  715. return (try? String(contentsOf: url)) ?? ""
  716. }
  717. func processAndSave(forecastData: [String: [Int]]) {
  718. let currentDate = Date()
  719. context.perform {
  720. for (type, values) in forecastData {
  721. self.createForecast(type: type, values: values, date: currentDate, context: self.context)
  722. }
  723. do {
  724. guard self.context.hasChanges else { return }
  725. try self.context.save()
  726. } catch {
  727. print(error.localizedDescription)
  728. }
  729. }
  730. }
  731. func createForecast(type: String, values: [Int], date: Date, context: NSManagedObjectContext) {
  732. let forecast = Forecast(context: context)
  733. forecast.id = UUID()
  734. forecast.date = date
  735. forecast.type = type
  736. for (index, value) in values.enumerated() {
  737. let forecastValue = ForecastValue(context: context)
  738. forecastValue.value = Int32(value)
  739. forecastValue.index = Int32(index)
  740. forecastValue.forecast = forecast
  741. }
  742. }
  743. }
  744. // Non-Async fetch methods for trio_custom_oref_variables
  745. extension OpenAPS {
  746. func fetchActiveTempTargets() throws -> [TempTargetStored] {
  747. try CoreDataStack.shared.fetchEntities(
  748. ofType: TempTargetStored.self,
  749. onContext: context,
  750. predicate: NSPredicate.lastActiveTempTarget,
  751. key: "date",
  752. ascending: false,
  753. fetchLimit: 1
  754. ) as? [TempTargetStored] ?? []
  755. }
  756. func fetchActiveOverrides() throws -> [OverrideStored] {
  757. try CoreDataStack.shared.fetchEntities(
  758. ofType: OverrideStored.self,
  759. onContext: context,
  760. predicate: NSPredicate.lastActiveOverride,
  761. key: "date",
  762. ascending: false,
  763. fetchLimit: 1
  764. ) as? [OverrideStored] ?? []
  765. }
  766. func fetchHistoricalTDDData(from date: Date) throws -> [[String: Any]] {
  767. try CoreDataStack.shared.fetchEntities(
  768. ofType: TDDStored.self,
  769. onContext: context,
  770. predicate: NSPredicate(format: "date > %@ AND total > 0", date as NSDate),
  771. key: "date",
  772. ascending: true,
  773. propertiesToFetch: ["date", "total"]
  774. ) as? [[String: Any]] ?? []
  775. }
  776. func fetchGlucose() throws -> [GlucoseStored] {
  777. let results = try CoreDataStack.shared.fetchEntities(
  778. ofType: GlucoseStored.self,
  779. onContext: context,
  780. predicate: NSPredicate.predicateFor30MinAgo,
  781. key: "date",
  782. ascending: false,
  783. fetchLimit: 4
  784. )
  785. return try context.perform {
  786. guard let glucoseResults = results as? [GlucoseStored] else {
  787. throw CoreDataError.fetchError(function: #function, file: #file)
  788. }
  789. return glucoseResults
  790. }
  791. }
  792. }