GlucoseStorage.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. import AVFAudio
  2. import Combine
  3. import CoreData
  4. import Foundation
  5. import SwiftDate
  6. import SwiftUI
  7. import Swinject
  8. protocol GlucoseStorage {
  9. var updatePublisher: AnyPublisher<Void, Never> { get }
  10. func storeGlucose(_ glucose: [BloodGlucose])
  11. func isGlucoseDataFresh(_ glucoseDate: Date?) -> Bool
  12. func syncDate() -> Date
  13. func filterTooFrequentGlucose(_ glucose: [BloodGlucose], at: Date) -> [BloodGlucose]
  14. func lastGlucoseDate() -> Date
  15. func isGlucoseFresh() -> Bool
  16. func getGlucoseNotYetUploadedToNightscout() async -> [BloodGlucose]
  17. func getCGMStateNotYetUploadedToNightscout() async -> [NightscoutTreatment]
  18. func getManualGlucoseNotYetUploadedToNightscout() async -> [NightscoutTreatment]
  19. var alarm: GlucoseAlarm? { get }
  20. }
  21. final class BaseGlucoseStorage: GlucoseStorage, Injectable {
  22. private let processQueue = DispatchQueue(label: "BaseGlucoseStorage.processQueue")
  23. @Injected() private var storage: FileStorage!
  24. @Injected() private var broadcaster: Broadcaster!
  25. @Injected() private var settingsManager: SettingsManager!
  26. let coredataContext = CoreDataStack.shared.newTaskContext()
  27. private let updateSubject = PassthroughSubject<Void, Never>()
  28. var updatePublisher: AnyPublisher<Void, Never> {
  29. updateSubject.eraseToAnyPublisher()
  30. }
  31. private enum Config {
  32. static let filterTime: TimeInterval = 3.5 * 60
  33. }
  34. init(resolver: Resolver) {
  35. injectServices(resolver)
  36. }
  37. private var glucoseFormatter: NumberFormatter {
  38. let formatter = NumberFormatter()
  39. formatter.numberStyle = .decimal
  40. formatter.maximumFractionDigits = 0
  41. if settingsManager.settings.units == .mmolL {
  42. formatter.maximumFractionDigits = 1
  43. }
  44. formatter.decimalSeparator = "."
  45. return formatter
  46. }
  47. func storeGlucose(_ glucose: [BloodGlucose]) {
  48. processQueue.sync {
  49. self.coredataContext.perform {
  50. let datesToCheck: Set<Date?> = Set(glucose.compactMap { $0.dateString as Date? })
  51. let fetchRequest: NSFetchRequest<NSFetchRequestResult> = GlucoseStored.fetchRequest()
  52. fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
  53. NSPredicate(format: "date IN %@", datesToCheck),
  54. NSPredicate.predicateForOneDayAgo
  55. ])
  56. fetchRequest.propertiesToFetch = ["date"]
  57. fetchRequest.resultType = .dictionaryResultType
  58. var existingDates = Set<Date>()
  59. do {
  60. let results = try self.coredataContext.fetch(fetchRequest) as? [NSDictionary]
  61. existingDates = Set(results?.compactMap({ $0["date"] as? Date }) ?? [])
  62. } catch {
  63. debugPrint("Failed to fetch existing glucose dates: \(error)")
  64. }
  65. var filteredGlucose = glucose.filter { !existingDates.contains($0.dateString) }
  66. // prepare batch insert
  67. let batchInsert = NSBatchInsertRequest(
  68. entity: GlucoseStored.entity(),
  69. managedObjectHandler: { (managedObject: NSManagedObject) -> Bool in
  70. guard let glucoseEntry = managedObject as? GlucoseStored, !filteredGlucose.isEmpty else {
  71. return true // Stop if there are no more items
  72. }
  73. let entry = filteredGlucose.removeFirst()
  74. glucoseEntry.id = UUID()
  75. glucoseEntry.glucose = Int16(entry.glucose ?? 0)
  76. glucoseEntry.date = entry.dateString
  77. glucoseEntry.direction = entry.direction?.rawValue
  78. glucoseEntry.isUploadedToNS = false /// the value is not uploaded to NS (yet)
  79. return false // Continue processing
  80. }
  81. )
  82. // process batch insert
  83. do {
  84. try self.coredataContext.execute(batchInsert)
  85. // Notify subscribers that there is a new glucose value
  86. // We need to do this because the due to the batch insert there is no ManagedObjectContext notification
  87. self.updateSubject.send(())
  88. } catch {
  89. debugPrint(
  90. "Glucose Storage: \(#function) \(DebuggingIdentifiers.failed) failed to execute batch insert: \(error)"
  91. )
  92. }
  93. debug(.deviceManager, "start storage cgmState")
  94. self.storage.transaction { storage in
  95. let file = OpenAPS.Monitor.cgmState
  96. var treatments = storage.retrieve(file, as: [NightscoutTreatment].self) ?? []
  97. var updated = false
  98. for x in glucose {
  99. debug(.deviceManager, "storeGlucose \(x)")
  100. guard let sessionStartDate = x.sessionStartDate else {
  101. continue
  102. }
  103. if let lastTreatment = treatments.last,
  104. let createdAt = lastTreatment.createdAt,
  105. // When a new Dexcom sensor is started, it produces multiple consecutive
  106. // startDates. Disambiguate them by only allowing a session start per minute.
  107. abs(createdAt.timeIntervalSince(sessionStartDate)) < TimeInterval(60)
  108. {
  109. continue
  110. }
  111. var notes = ""
  112. if let t = x.transmitterID {
  113. notes = t
  114. }
  115. if let a = x.activationDate {
  116. notes = "\(notes) activated on \(a)"
  117. }
  118. let treatment = NightscoutTreatment(
  119. duration: nil,
  120. rawDuration: nil,
  121. rawRate: nil,
  122. absolute: nil,
  123. rate: nil,
  124. eventType: .nsSensorChange,
  125. createdAt: sessionStartDate,
  126. enteredBy: NightscoutTreatment.local,
  127. bolus: nil,
  128. insulin: nil,
  129. notes: notes,
  130. carbs: nil,
  131. fat: nil,
  132. protein: nil,
  133. targetTop: nil,
  134. targetBottom: nil
  135. )
  136. debug(.deviceManager, "CGM sensor change \(treatment)")
  137. treatments.append(treatment)
  138. updated = true
  139. }
  140. if updated {
  141. // We have to keep quite a bit of history as sensors start only every 10 days.
  142. storage.save(
  143. treatments.filter
  144. { $0.createdAt != nil && $0.createdAt!.addingTimeInterval(30.days.timeInterval) > Date() },
  145. as: file
  146. )
  147. }
  148. }
  149. }
  150. }
  151. }
  152. func isGlucoseDataFresh(_ glucoseDate: Date?) -> Bool {
  153. guard let glucoseDate = glucoseDate else { return false }
  154. return glucoseDate > Date().addingTimeInterval(-6 * 60)
  155. }
  156. func syncDate() -> Date {
  157. let fr = GlucoseStored.fetchRequest()
  158. fr.predicate = NSPredicate.predicateForOneDayAgo
  159. fr.sortDescriptors = [NSSortDescriptor(keyPath: \GlucoseStored.date, ascending: false)]
  160. fr.fetchLimit = 1
  161. var date: Date?
  162. coredataContext.performAndWait {
  163. do {
  164. let results = try self.coredataContext.fetch(fr)
  165. date = results.first?.date
  166. } catch let error as NSError {
  167. print("Fetch error: \(DebuggingIdentifiers.failed) \(error.localizedDescription), \(error.userInfo)")
  168. }
  169. }
  170. return date ?? .distantPast
  171. }
  172. func lastGlucoseDate() -> Date {
  173. let fr = GlucoseStored.fetchRequest()
  174. fr.predicate = NSPredicate.predicateForOneDayAgo
  175. fr.sortDescriptors = [NSSortDescriptor(keyPath: \GlucoseStored.date, ascending: false)]
  176. fr.fetchLimit = 1
  177. var date: Date?
  178. coredataContext.performAndWait {
  179. do {
  180. let results = try self.coredataContext.fetch(fr)
  181. date = results.first?.date
  182. } catch let error as NSError {
  183. print("Fetch error: \(DebuggingIdentifiers.failed) \(error.localizedDescription), \(error.userInfo)")
  184. }
  185. }
  186. return date ?? .distantPast
  187. }
  188. func isGlucoseFresh() -> Bool {
  189. Date().timeIntervalSince(lastGlucoseDate()) <= Config.filterTime
  190. }
  191. func filterTooFrequentGlucose(_ glucose: [BloodGlucose], at date: Date) -> [BloodGlucose] {
  192. var lastDate = date
  193. var filtered: [BloodGlucose] = []
  194. let sorted = glucose.sorted { $0.date < $1.date }
  195. for entry in sorted {
  196. guard entry.dateString.addingTimeInterval(-Config.filterTime) > lastDate else {
  197. continue
  198. }
  199. filtered.append(entry)
  200. lastDate = entry.dateString
  201. }
  202. return filtered
  203. }
  204. func fetchLatestGlucose() -> GlucoseStored? {
  205. let predicate = NSPredicate.predicateFor20MinAgo
  206. return CoreDataStack.shared.fetchEntities(
  207. ofType: GlucoseStored.self,
  208. onContext: coredataContext,
  209. predicate: predicate,
  210. key: "date",
  211. ascending: false,
  212. fetchLimit: 1
  213. ).first
  214. }
  215. // Fetch glucose that is not uploaded to Nightscout yet
  216. /// Returns: Array of BloodGlucose to ensure the correct format for the NS Upload
  217. func getGlucoseNotYetUploadedToNightscout() async -> [BloodGlucose] {
  218. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  219. ofType: GlucoseStored.self,
  220. onContext: coredataContext,
  221. predicate: NSPredicate.glucoseNotYetUploadedToNightscout,
  222. key: "date",
  223. ascending: false,
  224. fetchLimit: 288
  225. )
  226. return await coredataContext.perform {
  227. guard let fetchedResults = results as? [GlucoseStored] else { return [] }
  228. return fetchedResults.map { result in
  229. BloodGlucose(
  230. _id: result.id?.uuidString ?? UUID().uuidString,
  231. sgv: Int(result.glucose),
  232. direction: BloodGlucose.Direction(from: result.direction ?? ""),
  233. date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
  234. dateString: result.date ?? Date(),
  235. unfiltered: Decimal(result.glucose),
  236. filtered: Decimal(result.glucose),
  237. noise: nil,
  238. glucose: Int(result.glucose)
  239. )
  240. }
  241. }
  242. }
  243. // Fetch manual glucose that is not uploaded to Nightscout yet
  244. /// Returns: Array of NightscoutTreatment to ensure the correct format for the NS Upload
  245. func getManualGlucoseNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
  246. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  247. ofType: GlucoseStored.self,
  248. onContext: coredataContext,
  249. predicate: NSPredicate.manualGlucoseNotYetUploadedToNightscout,
  250. key: "date",
  251. ascending: false,
  252. fetchLimit: 288
  253. )
  254. guard let fetchedResults = results as? [GlucoseStored] else { return [] }
  255. return await coredataContext.perform {
  256. return fetchedResults.map { result in
  257. NightscoutTreatment(
  258. duration: nil,
  259. rawDuration: nil,
  260. rawRate: nil,
  261. absolute: nil,
  262. rate: nil,
  263. eventType: .capillaryGlucose,
  264. createdAt: result.date,
  265. enteredBy: "Trio",
  266. bolus: nil,
  267. insulin: nil,
  268. notes: "Trio User",
  269. carbs: nil,
  270. fat: nil,
  271. protein: nil,
  272. foodType: nil,
  273. targetTop: nil,
  274. targetBottom: nil,
  275. glucoseType: "Manual",
  276. glucose: self.settingsManager.settings
  277. .units == .mgdL ? (self.glucoseFormatter.string(from: Int(result.glucose) as NSNumber) ?? "")
  278. : (self.glucoseFormatter.string(from: Decimal(result.glucose).asMmolL as NSNumber) ?? ""),
  279. units: self.settingsManager.settings.units == .mmolL ? "mmol" : "mg/dl",
  280. id: result.id?.uuidString
  281. )
  282. }
  283. }
  284. }
  285. func getCGMStateNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
  286. async let alreadyUploaded: [NightscoutTreatment] = storage
  287. .retrieveAsync(OpenAPS.Nightscout.uploadedCGMState, as: [NightscoutTreatment].self) ?? []
  288. async let allValues: [NightscoutTreatment] = storage
  289. .retrieveAsync(OpenAPS.Monitor.cgmState, as: [NightscoutTreatment].self) ?? []
  290. let (alreadyUploadedValues, allValuesSet) = await (alreadyUploaded, allValues)
  291. return Array(Set(allValuesSet).subtracting(Set(alreadyUploadedValues)))
  292. }
  293. var alarm: GlucoseAlarm? {
  294. /// glucose can not be older than 20 minutes due to the predicate in the fetch request
  295. coredataContext.performAndWait {
  296. guard let glucose = fetchLatestGlucose() else { return nil }
  297. let glucoseValue = glucose.glucose
  298. if Decimal(glucoseValue) <= settingsManager.settings.lowGlucose {
  299. return .low
  300. }
  301. if Decimal(glucoseValue) >= settingsManager.settings.highGlucose {
  302. return .high
  303. }
  304. return nil
  305. }
  306. }
  307. }
  308. protocol GlucoseObserver {
  309. func glucoseDidUpdate(_ glucose: [BloodGlucose])
  310. }
  311. enum GlucoseAlarm {
  312. case high
  313. case low
  314. var displayName: String {
  315. switch self {
  316. case .high:
  317. return NSLocalizedString("LOWALERT!", comment: "LOWALERT!")
  318. case .low:
  319. return NSLocalizedString("HIGHALERT!", comment: "HIGHALERT!")
  320. }
  321. }
  322. }