GlucoseStorage.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. import AVFAudio
  2. import CoreData
  3. import Foundation
  4. import SwiftDate
  5. import SwiftUI
  6. import Swinject
  7. protocol GlucoseStorage {
  8. func storeGlucose(_ glucose: [BloodGlucose])
  9. func syncDate() -> Date
  10. func filterTooFrequentGlucose(_ glucose: [BloodGlucose], at: Date) -> [BloodGlucose]
  11. func lastGlucoseDate() -> Date
  12. func isGlucoseFresh() -> Bool
  13. func nightscoutGlucoseNotUploaded() -> [BloodGlucose]
  14. func nightscoutCGMStateNotUploaded() -> [NigtscoutTreatment]
  15. func nightscoutManualGlucoseNotUploaded() -> [NigtscoutTreatment]
  16. var alarm: GlucoseAlarm? { get }
  17. }
  18. final class BaseGlucoseStorage: GlucoseStorage, Injectable {
  19. private let processQueue = DispatchQueue(label: "BaseGlucoseStorage.processQueue")
  20. @Injected() private var storage: FileStorage!
  21. @Injected() private var broadcaster: Broadcaster!
  22. @Injected() private var settingsManager: SettingsManager!
  23. let coredataContext = CoreDataStack.shared.backgroundContext
  24. private enum Config {
  25. static let filterTime: TimeInterval = 3.5 * 60
  26. }
  27. init(resolver: Resolver) {
  28. injectServices(resolver)
  29. }
  30. private var glucoseFormatter: NumberFormatter {
  31. let formatter = NumberFormatter()
  32. formatter.numberStyle = .decimal
  33. formatter.maximumFractionDigits = 0
  34. if settingsManager.settings.units == .mmolL {
  35. formatter.maximumFractionDigits = 1
  36. }
  37. formatter.decimalSeparator = "."
  38. return formatter
  39. }
  40. func storeGlucose(_ glucose: [BloodGlucose]) {
  41. processQueue.sync {
  42. debug(.deviceManager, "start storage glucose")
  43. self.coredataContext.perform {
  44. // read
  45. let datesToCheck: Set<Date> = Set(glucose.compactMap { $0.dateString as Date? })
  46. let fetchRequest: NSFetchRequest<NSFetchRequestResult> = GlucoseStored.fetchRequest()
  47. fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
  48. NSPredicate(format: "date IN %@", datesToCheck),
  49. NSPredicate.predicateForOneDayAgo
  50. ])
  51. fetchRequest.propertiesToFetch = ["date"]
  52. fetchRequest.resultType = .dictionaryResultType
  53. var existingDates = Set<Date>()
  54. do {
  55. let results = try self.coredataContext.fetch(fetchRequest) as? [NSDictionary]
  56. existingDates = Set(results?.compactMap({ $0["date"] as? Date }) ?? [])
  57. } catch {
  58. debugPrint("Failed to fetch existing glucose dates: \(error)")
  59. }
  60. // filtering before loop
  61. let filteredGlucose = glucose.filter { glucoseEntry -> Bool in
  62. !existingDates.contains(glucoseEntry.dateString)
  63. }
  64. // save
  65. for glucoseEntry in filteredGlucose {
  66. guard let glucoseValue = glucoseEntry.glucose else { continue }
  67. let newItem = GlucoseStored(context: self.coredataContext)
  68. newItem.id = UUID()
  69. newItem.glucose = Int16(glucoseValue)
  70. newItem.date = glucoseEntry.dateString
  71. newItem.direction = glucoseEntry.direction?.symbol
  72. }
  73. if self.coredataContext.hasChanges {
  74. do {
  75. try self.coredataContext.save()
  76. debugPrint(
  77. "Glucose Storage: \(CoreDataStack.identifier) \(DebuggingIdentifiers.succeeded) saved glucose to core data"
  78. )
  79. } catch {
  80. debugPrint(
  81. "Glucose Storage: \(CoreDataStack.identifier) \(DebuggingIdentifiers.failed) failed to save glucose to core data: \(error)"
  82. )
  83. }
  84. }
  85. }
  86. debug(.deviceManager, "start storage cgmState")
  87. self.storage.transaction { storage in
  88. let file = OpenAPS.Monitor.cgmState
  89. var treatments = storage.retrieve(file, as: [NigtscoutTreatment].self) ?? []
  90. var updated = false
  91. for x in glucose {
  92. debug(.deviceManager, "storeGlucose \(x)")
  93. guard let sessionStartDate = x.sessionStartDate else {
  94. continue
  95. }
  96. if let lastTreatment = treatments.last,
  97. let createdAt = lastTreatment.createdAt,
  98. // When a new Dexcom sensor is started, it produces multiple consequetive
  99. // startDates. Disambiguate them by only allowing a session start per minute.
  100. abs(createdAt.timeIntervalSince(sessionStartDate)) < TimeInterval(60)
  101. {
  102. continue
  103. }
  104. var notes = ""
  105. if let t = x.transmitterID {
  106. notes = t
  107. }
  108. if let a = x.activationDate {
  109. notes = "\(notes) activated on \(a)"
  110. }
  111. let treatment = NigtscoutTreatment(
  112. duration: nil,
  113. rawDuration: nil,
  114. rawRate: nil,
  115. absolute: nil,
  116. rate: nil,
  117. eventType: .nsSensorChange,
  118. createdAt: sessionStartDate,
  119. enteredBy: NigtscoutTreatment.local,
  120. bolus: nil,
  121. insulin: nil,
  122. notes: notes,
  123. carbs: nil,
  124. fat: nil,
  125. protein: nil,
  126. targetTop: nil,
  127. targetBottom: nil
  128. )
  129. debug(.deviceManager, "CGM sensor change \(treatment)")
  130. treatments.append(treatment)
  131. updated = true
  132. }
  133. if updated {
  134. // We have to keep quite a bit of history as sensors start only every 10 days.
  135. storage.save(
  136. treatments.filter
  137. { $0.createdAt != nil && $0.createdAt!.addingTimeInterval(30.days.timeInterval) > Date() },
  138. as: file
  139. )
  140. }
  141. }
  142. }
  143. }
  144. func syncDate() -> Date {
  145. // TODO: - proof logic here!
  146. fetchGlucose().first?.date ?? .distantPast
  147. }
  148. func lastGlucoseDate() -> Date {
  149. fetchGlucose().first?.date ?? .distantPast
  150. }
  151. func isGlucoseFresh() -> Bool {
  152. Date().timeIntervalSince(lastGlucoseDate()) <= Config.filterTime
  153. }
  154. func filterTooFrequentGlucose(_ glucose: [BloodGlucose], at date: Date) -> [BloodGlucose] {
  155. var lastDate = date
  156. var filtered: [BloodGlucose] = []
  157. let sorted = glucose.sorted { $0.date < $1.date }
  158. for entry in sorted {
  159. guard entry.dateString.addingTimeInterval(-Config.filterTime) > lastDate else {
  160. continue
  161. }
  162. filtered.append(entry)
  163. lastDate = entry.dateString
  164. }
  165. return filtered
  166. }
  167. // MARK: - fetching non manual Glucose, manual Glucose and the last glucose value
  168. // TODO: -optimize this bullshit here...I would love to use the async/await pattern, but its simply not possible because you would need to change all the calls of the following functions and make them async...same shit with the NSAsynchronousFetchRequest
  169. /// its all done on a background thread and on a separate queue so hopefully its not too heavy
  170. /// also tried this but here again you need to make everything asynchronous...
  171. /// let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
  172. /// privateContext.parent = coredataContext /// merges changes to the core data context
  173. private func fetchGlucose() -> [GlucoseStored] {
  174. do {
  175. debugPrint("OpenAPS: \(#function) \(DebuggingIdentifiers.succeeded) fetched glucose")
  176. return try coredataContext.fetch(GlucoseStored.fetch(
  177. NSPredicate.predicateForOneDayAgo,
  178. ascending: false,
  179. fetchLimit: 288,
  180. batchSize: 50
  181. ))
  182. } catch {
  183. debugPrint("OpenAPS: \(#function) \(DebuggingIdentifiers.failed) failed to fetch glucose")
  184. return []
  185. }
  186. }
  187. private func fetchLatestGlucose() -> GlucoseStored? {
  188. do {
  189. debugPrint("Glucose Storage: \(#function) \(DebuggingIdentifiers.succeeded) fetched glucose")
  190. return try coredataContext.fetch(GlucoseStored.fetch(
  191. NSPredicate.predicateFor20MinAgo,
  192. ascending: false,
  193. fetchLimit: 1
  194. )).first
  195. } catch {
  196. debugPrint("Glucose Storage: \(#function) \(DebuggingIdentifiers.failed) failed to fetch glucose")
  197. return nil
  198. }
  199. }
  200. private func fetchAndProcessManualGlucose() -> [BloodGlucose] {
  201. do {
  202. let fetchedResults = try coredataContext.fetch(GlucoseStored.fetch(
  203. NSPredicate.manualGlucose,
  204. ascending: false,
  205. fetchLimit: 288,
  206. batchSize: 50
  207. ))
  208. debugPrint("Glucose Storage: \(#function) \(DebuggingIdentifiers.succeeded) fetched manual glucose")
  209. let glucoseArray = fetchedResults.map { result in
  210. BloodGlucose(
  211. date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
  212. dateString: result.date ?? Date(),
  213. unfiltered: Decimal(result.glucose),
  214. filtered: Decimal(result.glucose),
  215. noise: nil,
  216. type: ""
  217. )
  218. }
  219. return glucoseArray
  220. } catch {
  221. debugPrint("Glucose Storage: \(#function) \(DebuggingIdentifiers.failed) failed to fetch manual glucose")
  222. return []
  223. }
  224. }
  225. private func fetchAndProcessGlucose() -> [BloodGlucose] {
  226. do {
  227. let results = try coredataContext.fetch(GlucoseStored.fetch(
  228. NSPredicate.predicateForOneDayAgo,
  229. ascending: false,
  230. fetchLimit: 288,
  231. batchSize: 50
  232. ))
  233. debugPrint("Glucose Storage: \(#function) \(DebuggingIdentifiers.succeeded) fetched glucose")
  234. let glucoseArray = results.map { result in
  235. BloodGlucose(
  236. date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
  237. dateString: result.date ?? Date(),
  238. unfiltered: Decimal(result.glucose),
  239. filtered: Decimal(result.glucose),
  240. noise: nil,
  241. type: ""
  242. )
  243. }
  244. return glucoseArray
  245. } catch {
  246. debugPrint("Glucose Storage: \(#function) \(DebuggingIdentifiers.failed) failed to fetch glucose")
  247. return []
  248. }
  249. }
  250. func nightscoutGlucoseNotUploaded() -> [BloodGlucose] {
  251. let uploaded = storage.retrieve(OpenAPS.Nightscout.uploadedGlucose, as: [BloodGlucose].self) ?? []
  252. let recentGlucose = fetchAndProcessGlucose()
  253. return Array(Set(recentGlucose).subtracting(Set(uploaded)))
  254. }
  255. func nightscoutCGMStateNotUploaded() -> [NigtscoutTreatment] {
  256. let uploaded = storage.retrieve(OpenAPS.Nightscout.uploadedCGMState, as: [NigtscoutTreatment].self) ?? []
  257. let recent = storage.retrieve(OpenAPS.Monitor.cgmState, as: [NigtscoutTreatment].self) ?? []
  258. return Array(Set(recent).subtracting(Set(uploaded)))
  259. }
  260. func nightscoutManualGlucoseNotUploaded() -> [NigtscoutTreatment] {
  261. let uploaded = (storage.retrieve(OpenAPS.Nightscout.uploadedGlucose, as: [BloodGlucose].self) ?? [])
  262. .filter({ $0.type == GlucoseType.manual.rawValue })
  263. let recent = fetchAndProcessManualGlucose()
  264. let filtered = Array(Set(recent).subtracting(Set(uploaded)))
  265. let manualReadings = filtered.map { item -> NigtscoutTreatment in
  266. NigtscoutTreatment(
  267. duration: nil, rawDuration: nil, rawRate: nil, absolute: nil, rate: nil, eventType: .capillaryGlucose,
  268. createdAt: item.dateString, enteredBy: "iAPS", bolus: nil, insulin: nil, notes: "iAPS User", carbs: nil,
  269. fat: nil,
  270. protein: nil, foodType: nil, targetTop: nil, targetBottom: nil, glucoseType: "Manual",
  271. glucose: settingsManager.settings
  272. .units == .mgdL ? (glucoseFormatter.string(from: Int(item.glucose ?? 100) as NSNumber) ?? "")
  273. : (glucoseFormatter.string(from: Decimal(item.glucose ?? 100).asMmolL as NSNumber) ?? ""),
  274. units: settingsManager.settings.units == .mmolL ? "mmol" : "mg/dl"
  275. )
  276. }
  277. return manualReadings
  278. }
  279. var alarm: GlucoseAlarm? {
  280. /// glucose can not be older than 20 minutes due to the predicate in the fetch request
  281. guard let glucose = fetchLatestGlucose() else { return nil }
  282. let glucoseValue = glucose.glucose
  283. if Decimal(glucoseValue) <= settingsManager.settings.lowGlucose {
  284. return .low
  285. }
  286. if Decimal(glucoseValue) >= settingsManager.settings.highGlucose {
  287. return .high
  288. }
  289. return nil
  290. }
  291. }
  292. protocol GlucoseObserver {
  293. func glucoseDidUpdate(_ glucose: [BloodGlucose])
  294. }
  295. enum GlucoseAlarm {
  296. case high
  297. case low
  298. var displayName: String {
  299. switch self {
  300. case .high:
  301. return NSLocalizedString("LOWALERT!", comment: "LOWALERT!")
  302. case .low:
  303. return NSLocalizedString("HIGHALERT!", comment: "HIGHALERT!")
  304. }
  305. }
  306. }