| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695 |
- import AVFAudio
- import Combine
- import CoreData
- import Foundation
- import LoopKit
- import SwiftDate
- import SwiftUI
- import Swinject
- protocol GlucoseStorage {
- var updatePublisher: AnyPublisher<Void, Never> { get }
- func storeGlucose(_ glucose: [BloodGlucose]) async throws
- func backfillGlucose(_ glucose: [BloodGlucose]) async throws
- func addManualGlucose(glucose: Int)
- func isGlucoseDataFresh(_ glucoseDate: Date?) -> Bool
- func syncDate() -> Date
- func filterTooFrequentGlucose(_ glucose: [BloodGlucose], at: Date) -> [BloodGlucose]
- func lastGlucoseDate() -> Date?
- func isGlucoseFresh() -> Bool
- func getGlucoseNotYetUploadedToNightscout() async throws -> [BloodGlucose]
- func getCGMStateNotYetUploadedToNightscout() async throws -> [NightscoutTreatment]
- func getManualGlucoseNotYetUploadedToNightscout() async throws -> [NightscoutTreatment]
- func getGlucoseNotYetUploadedToHealth() async throws -> [BloodGlucose]
- func getManualGlucoseNotYetUploadedToHealth() async throws -> [BloodGlucose]
- func getGlucoseNotYetUploadedToTidepool() async throws -> [StoredGlucoseSample]
- func getManualGlucoseNotYetUploadedToTidepool() async throws -> [StoredGlucoseSample]
- var alarm: GlucoseAlarm? { get }
- func deleteGlucose(_ treatmentObjectID: NSManagedObjectID) async
- }
- final class BaseGlucoseStorage: GlucoseStorage, Injectable {
- private let processQueue = DispatchQueue(label: "BaseGlucoseStorage.processQueue")
- @Injected() private var storage: FileStorage!
- @Injected() private var broadcaster: Broadcaster!
- @Injected() private var settingsManager: SettingsManager!
- private let updateSubject = PassthroughSubject<Void, Never>()
- var updatePublisher: AnyPublisher<Void, Never> {
- updateSubject.eraseToAnyPublisher()
- }
- private enum Config {
- static let filterTime: TimeInterval = 3.5 * 60
- }
- private let context: NSManagedObjectContext
- init(resolver: Resolver, context: NSManagedObjectContext? = nil) {
- self.context = context ?? CoreDataStack.shared.newTaskContext()
- injectServices(resolver)
- }
- private var glucoseFormatter: NumberFormatter {
- let formatter = NumberFormatter()
- formatter.numberStyle = .decimal
- formatter.maximumFractionDigits = 0
- if settingsManager.settings.units == .mmolL {
- formatter.maximumFractionDigits = 1
- }
- formatter.decimalSeparator = "."
- return formatter
- }
- /// Backfills glucose values and stores in CoreData
- ///
- /// CGM managers will sometimes backfill glucose readings. To handle these backfilled values
- /// correctly, we need some logic to handle a few cases:
- /// - _Not_ adding back previously deleted glucose
- /// - Avoiding duplicate values for the same reading
- /// - Avoiding overlapping glucose readings when switching sources
- /// Of these corner cases, overlapping glucose readings when switching sources is both
- /// the most challenging and most rare since it would happen if wearing two devices and
- /// switching or moving from direct glucose handling to xdrip. It's not worth the complexity
- /// to deal with source switching perfectly, so instead we will backfill glucose if and only if
- /// it isn't within 3.5 minutes of an existing glucose reading, which is simple but not perfect.
- /// But since this is a corner case that really shouldn't happen often, it's good enough.
- func backfillGlucose(_ glucose: [BloodGlucose]) async throws {
- try await context.perform {
- // remove already deleted glucose values
- let withoutDeletedGlucose = self.filterGlucoseValues(
- glucose,
- fetchRequest: DeletedGlucoseStored.fetchRequest(),
- timeBuffer: 1
- )
- // check for a 3.5 minute difference between existing values
- let filteredGlucose = self.filterGlucoseValues(
- withoutDeletedGlucose,
- fetchRequest: GlucoseStored.fetchRequest(),
- timeBuffer: 3.5 * 60
- )
- guard !filteredGlucose.isEmpty else { return }
- do {
- // Store glucose values in Core Data
- try self.storeGlucoseInCoreData(filteredGlucose)
- } catch {
- throw CoreDataError.creationError(
- function: #function,
- file: #fileID
- )
- }
- }
- }
- func storeGlucose(_ glucose: [BloodGlucose]) async throws {
- try await context.perform {
- // Get new glucose values that don't exist yet
- let newGlucose = self.filterGlucoseValues(glucose, fetchRequest: GlucoseStored.fetchRequest(), timeBuffer: 1)
- guard !newGlucose.isEmpty else { return }
- do {
- // Store glucose values in Core Data
- try self.storeGlucoseInCoreData(newGlucose)
- } catch {
- throw CoreDataError.creationError(
- function: #function,
- file: #fileID
- )
- }
- // Store CGM state if needed
- self.storeCGMState(glucose)
- }
- }
- /// filter out duplicate CGM readings using matching timestamps
- ///
- /// This function will fetch dates from the `fetchRequest` and remove any glucose
- /// values that are within `timeBuffer` of the fetched dates. This logic is useful for
- /// deduplication checks or removing deleted CGM values from a list of backfilled readings.
- private func filterGlucoseValues(
- _ glucose: [BloodGlucose],
- fetchRequest: NSFetchRequest<NSFetchRequestResult>,
- timeBuffer: TimeInterval
- ) -> [BloodGlucose] {
- let datesToCheck = glucose.map(\.dateString).sorted()
- guard let firstDate = datesToCheck.first.map({ $0.addingTimeInterval(-timeBuffer) }),
- let lastDate = datesToCheck.last.map({ $0.addingTimeInterval(timeBuffer) })
- else {
- return glucose
- }
- fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
- NSPredicate(format: "date >= %@", firstDate as NSDate),
- NSPredicate(format: "date <= %@", lastDate as NSDate)
- ])
- fetchRequest.propertiesToFetch = ["date"]
- fetchRequest.resultType = .dictionaryResultType
- var existingDates = [Date]()
- do {
- let results = try context.fetch(fetchRequest) as? [NSDictionary]
- existingDates = results?.compactMap({ $0["date"] as? Date }) ?? []
- } catch {
- debugPrint("Failed to fetch existing glucose dates: \(error)")
- }
- // This is an inefficient filtering algorithm, but I'm assuming that the
- // time spans are short and that duplicates are rare, so in the common
- // case there won't be any existing dates.
- return glucose.filter { glucose in
- for existingDate in existingDates {
- let difference = abs(existingDate.timeIntervalSince(glucose.dateString))
- if difference <= timeBuffer {
- return false
- }
- }
- return true
- }
- }
- private func storeGlucoseInCoreData(_ glucose: [BloodGlucose]) throws {
- if glucose.count > 1 {
- try storeGlucoseBatch(glucose)
- } else {
- try storeGlucoseRegular(glucose)
- }
- }
- private func storeGlucoseRegular(_ glucose: [BloodGlucose]) throws {
- for entry in glucose {
- let glucoseEntry = GlucoseStored(context: context)
- configureGlucoseEntry(glucoseEntry, with: entry)
- }
- guard context.hasChanges else { return }
- try context.save()
- }
- private func storeGlucoseBatch(_ glucose: [BloodGlucose]) throws {
- var remainingGlucose = glucose
- let batchInsert = NSBatchInsertRequest(
- entity: GlucoseStored.entity(),
- managedObjectHandler: { (managedObject: NSManagedObject) -> Bool in
- guard let glucoseEntry = managedObject as? GlucoseStored,
- !remainingGlucose.isEmpty
- else {
- return true
- }
- let entry = remainingGlucose.removeFirst()
- self.configureGlucoseEntry(glucoseEntry, with: entry)
- return false
- }
- )
- try context.execute(batchInsert)
- // Only send update for batch insert since regular save triggers CoreData notifications
- updateSubject.send()
- }
- private func configureGlucoseEntry(_ entry: GlucoseStored, with glucose: BloodGlucose) {
- entry.id = UUID()
- entry.glucose = Int16(glucose.glucose ?? 0)
- entry.date = glucose.dateString
- entry.direction = glucose.direction?.rawValue
- entry.isUploadedToNS = false
- entry.isUploadedToHealth = false
- entry.isUploadedToTidepool = false
- }
- private func storeCGMState(_ glucose: [BloodGlucose]) {
- debug(.deviceManager, "start storage cgmState")
- storage.transaction { storage in
- let file = OpenAPS.Monitor.cgmState
- var treatments = storage.retrieve(file, as: [NightscoutTreatment].self) ?? []
- var updated = false
- for x in glucose {
- guard let sessionStartDate = x.sessionStartDate else { continue }
- // Skip if we already have a recent treatment
- if let lastTreatment = treatments.last,
- let createdAt = lastTreatment.createdAt,
- abs(createdAt.timeIntervalSince(sessionStartDate)) < TimeInterval(60)
- {
- continue
- }
- let notes = createCGMStateNotes(transmitterID: x.transmitterID, activationDate: x.activationDate)
- let treatment = createCGMStateTreatment(sessionStartDate: sessionStartDate, notes: notes)
- debug(.deviceManager, "CGM sensor change \(treatment)")
- treatments.append(treatment)
- updated = true
- }
- if updated {
- storage.save(
- treatments.filter { $0.createdAt?.addingTimeInterval(30.days.timeInterval) ?? .distantPast > Date() },
- as: file
- )
- }
- }
- }
- private func createCGMStateNotes(transmitterID: String?, activationDate: Date?) -> String {
- var notes = ""
- if let t = transmitterID {
- notes = t
- }
- if let a = activationDate {
- notes = "\(notes) activated on \(a)"
- }
- return notes
- }
- private func createCGMStateTreatment(sessionStartDate: Date, notes: String) -> NightscoutTreatment {
- NightscoutTreatment(
- duration: nil,
- rawDuration: nil,
- rawRate: nil,
- absolute: nil,
- rate: nil,
- eventType: .nsSensorChange,
- createdAt: sessionStartDate,
- enteredBy: NightscoutTreatment.local,
- bolus: nil,
- insulin: nil,
- notes: notes,
- carbs: nil,
- fat: nil,
- protein: nil,
- targetTop: nil,
- targetBottom: nil
- )
- }
- func addManualGlucose(glucose: Int) {
- context.perform {
- let newItem = GlucoseStored(context: self.context)
- newItem.id = UUID()
- newItem.date = Date()
- newItem.glucose = Int16(glucose)
- newItem.isManual = true
- newItem.isUploadedToNS = false
- newItem.isUploadedToHealth = false
- newItem.isUploadedToTidepool = false
- do {
- guard self.context.hasChanges else { return }
- try self.context.save()
- // Glucose subscribers already listen to the update publisher, so call here to update glucose-related data.
- self.updateSubject.send()
- } catch let error as NSError {
- debugPrint(
- "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save manual glucose to Core Data with error: \(error)"
- )
- }
- }
- }
- func isGlucoseDataFresh(_ glucoseDate: Date?) -> Bool {
- guard let glucoseDate = glucoseDate else { return false }
- return glucoseDate > Date().addingTimeInterval(-6 * 60)
- }
- func syncDate() -> Date {
- // Optimize fetch request to only get the date
- let taskContext = CoreDataStack.shared.newTaskContext()
- let fr = NSFetchRequest<NSDictionary>(entityName: "GlucoseStored")
- fr.predicate = NSPredicate.predicateForOneDayAgo
- fr.propertiesToFetch = ["date"]
- fr.fetchLimit = 1
- fr.resultType = .dictionaryResultType
- fr.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)]
- var fetchedDate: Date = .distantPast
- taskContext.performAndWait {
- do {
- if let result = try taskContext.fetch(fr).first,
- let date = result["date"] as? Date
- {
- fetchedDate = date
- }
- } catch {
- debugPrint("Fetch error: \(DebuggingIdentifiers.failed) \(error)")
- }
- }
- return fetchedDate
- }
- func lastGlucoseDate() -> Date? {
- let fetchRequest = GlucoseStored.fetchRequest()
- fetchRequest.predicate = NSPredicate.predicateForOneDayAgo
- fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \GlucoseStored.date, ascending: false)]
- fetchRequest.fetchLimit = 1
- var date: Date?
- context.performAndWait {
- do {
- let results = try self.context.fetch(fetchRequest)
- date = results.first?.date
- } catch let error as NSError {
- debug(.storage, "Fetch error: \(DebuggingIdentifiers.failed) \(error), \(error.userInfo)")
- }
- }
- return date
- }
- func isGlucoseFresh() -> Bool {
- Date().timeIntervalSince(lastGlucoseDate() ?? .distantPast) <= Config.filterTime
- }
- func filterTooFrequentGlucose(_ glucose: [BloodGlucose], at date: Date) -> [BloodGlucose] {
- var lastDate = date
- var filtered: [BloodGlucose] = []
- let sorted = glucose.sorted { $0.date < $1.date }
- for entry in sorted {
- guard entry.dateString.addingTimeInterval(-Config.filterTime) > lastDate else {
- continue
- }
- filtered.append(entry)
- lastDate = entry.dateString
- }
- return filtered
- }
- func fetchLatestGlucose() throws -> GlucoseStored? {
- let predicate = NSPredicate.predicateFor20MinAgo
- return (try CoreDataStack.shared.fetchEntities(
- ofType: GlucoseStored.self,
- onContext: context,
- predicate: predicate,
- key: "date",
- ascending: false,
- fetchLimit: 1
- ) as? [GlucoseStored] ?? []).first
- }
- // Fetch glucose that is not uploaded to Nightscout yet
- /// - Returns: Array of BloodGlucose to ensure the correct format for the NS Upload
- func getGlucoseNotYetUploadedToNightscout() async throws -> [BloodGlucose] {
- let results = try await CoreDataStack.shared.fetchEntitiesAsync(
- ofType: GlucoseStored.self,
- onContext: context,
- predicate: NSPredicate.glucoseNotYetUploadedToNightscout,
- key: "date",
- ascending: false
- )
- return try await context.perform {
- guard let fetchedResults = results as? [GlucoseStored] else {
- throw CoreDataError.fetchError(function: #function, file: #file)
- }
- return fetchedResults.map { result in
- BloodGlucose(
- id: result.id?.uuidString ?? UUID().uuidString,
- sgv: Int(result.glucose),
- direction: BloodGlucose.Direction(from: result.direction ?? ""),
- date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
- dateString: result.date ?? Date(),
- unfiltered: Decimal(result.glucose),
- filtered: Decimal(result.glucose),
- noise: nil,
- glucose: Int(result.glucose),
- type: "sgv"
- )
- }
- }
- }
- // Fetch manual glucose that is not uploaded to Nightscout yet
- /// - Returns: Array of NightscoutTreatment to ensure the correct format for the NS Upload
- func getManualGlucoseNotYetUploadedToNightscout() async throws -> [NightscoutTreatment] {
- let results = try await CoreDataStack.shared.fetchEntitiesAsync(
- ofType: GlucoseStored.self,
- onContext: context,
- predicate: NSPredicate.manualGlucoseNotYetUploadedToNightscout,
- key: "date",
- ascending: false
- )
- return try await context.perform {
- guard let fetchedResults = results as? [GlucoseStored] else {
- throw CoreDataError.fetchError(function: #function, file: #file)
- }
- return fetchedResults.map { result in
- NightscoutTreatment(
- duration: nil,
- rawDuration: nil,
- rawRate: nil,
- absolute: nil,
- rate: nil,
- eventType: .capillaryGlucose,
- createdAt: result.date,
- enteredBy: CarbsEntry.local,
- bolus: nil,
- insulin: nil,
- notes: "Trio User",
- carbs: nil,
- fat: nil,
- protein: nil,
- foodType: nil,
- targetTop: nil,
- targetBottom: nil,
- glucoseType: "Manual",
- glucose: self.settingsManager.settings
- .units == .mgdL ? (self.glucoseFormatter.string(from: Int(result.glucose) as NSNumber) ?? "")
- : (self.glucoseFormatter.string(from: Decimal(result.glucose).asMmolL as NSNumber) ?? ""),
- units: self.settingsManager.settings.units == .mmolL ? "mmol" : "mg/dl",
- id: result.id?.uuidString
- )
- }
- }
- }
- func getCGMStateNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
- async let alreadyUploaded: [NightscoutTreatment] = storage
- .retrieveAsync(OpenAPS.Nightscout.uploadedCGMState, as: [NightscoutTreatment].self) ?? []
- async let allValues: [NightscoutTreatment] = storage
- .retrieveAsync(OpenAPS.Monitor.cgmState, as: [NightscoutTreatment].self) ?? []
- let (alreadyUploadedValues, allValuesSet) = await (alreadyUploaded, allValues)
- return Array(Set(allValuesSet).subtracting(Set(alreadyUploadedValues)))
- }
- // Fetch glucose that is not uploaded to Nightscout yet
- /// - Returns: Array of BloodGlucose to ensure the correct format for the NS Upload
- func getGlucoseNotYetUploadedToHealth() async throws -> [BloodGlucose] {
- let results = try await CoreDataStack.shared.fetchEntitiesAsync(
- ofType: GlucoseStored.self,
- onContext: context,
- predicate: NSPredicate.glucoseNotYetUploadedToHealth,
- key: "date",
- ascending: false
- )
- return try await context.perform {
- guard let fetchedResults = results as? [GlucoseStored] else {
- throw CoreDataError.fetchError(function: #function, file: #file)
- }
- return fetchedResults.map { result in
- BloodGlucose(
- id: result.id?.uuidString ?? UUID().uuidString,
- sgv: Int(result.glucose),
- direction: BloodGlucose.Direction(from: result.direction ?? ""),
- date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
- dateString: result.date ?? Date(),
- unfiltered: Decimal(result.glucose),
- filtered: Decimal(result.glucose),
- noise: nil,
- glucose: Int(result.glucose)
- )
- }
- }
- }
- // Fetch manual glucose that is not uploaded to Nightscout yet
- /// - Returns: Array of NightscoutTreatment to ensure the correct format for the NS Upload
- func getManualGlucoseNotYetUploadedToHealth() async throws -> [BloodGlucose] {
- let results = try await CoreDataStack.shared.fetchEntitiesAsync(
- ofType: GlucoseStored.self,
- onContext: context,
- predicate: NSPredicate.manualGlucoseNotYetUploadedToHealth,
- key: "date",
- ascending: false
- )
- return try await context.perform {
- guard let fetchedResults = results as? [GlucoseStored] else {
- throw CoreDataError.fetchError(function: #function, file: #file)
- }
- return fetchedResults.map { result in
- BloodGlucose(
- id: result.id?.uuidString ?? UUID().uuidString,
- sgv: Int(result.glucose),
- direction: BloodGlucose.Direction(from: result.direction ?? ""),
- date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
- dateString: result.date ?? Date(),
- unfiltered: Decimal(result.glucose),
- filtered: Decimal(result.glucose),
- noise: nil,
- glucose: Int(result.glucose)
- )
- }
- }
- }
- // Fetch glucose that is not uploaded to Tidepool yet
- /// - Returns: Array of StoredGlucoseSample to ensure the correct format for Tidepool upload
- func getGlucoseNotYetUploadedToTidepool() async throws -> [StoredGlucoseSample] {
- let results = try await CoreDataStack.shared.fetchEntitiesAsync(
- ofType: GlucoseStored.self,
- onContext: context,
- predicate: NSPredicate.glucoseNotYetUploadedToTidepool,
- key: "date",
- ascending: false
- )
- return try await context.perform {
- guard let fetchedResults = results as? [GlucoseStored] else {
- throw CoreDataError.fetchError(function: #function, file: #file)
- }
- return fetchedResults.map { result in
- BloodGlucose(
- id: result.id?.uuidString ?? UUID().uuidString,
- sgv: Int(result.glucose),
- direction: BloodGlucose.Direction(from: result.direction ?? ""),
- date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
- dateString: result.date ?? Date(),
- unfiltered: Decimal(result.glucose),
- filtered: Decimal(result.glucose),
- noise: nil,
- glucose: Int(result.glucose)
- )
- }
- .map { $0.convertStoredGlucoseSample(isManualGlucose: false) }
- }
- }
- // Fetch manual glucose that is not uploaded to Tidepool yet
- /// - Returns: Array of StoredGlucoseSample to ensure the correct format for the Tidepool upload
- func getManualGlucoseNotYetUploadedToTidepool() async throws -> [StoredGlucoseSample] {
- let results = try await CoreDataStack.shared.fetchEntitiesAsync(
- ofType: GlucoseStored.self,
- onContext: context,
- predicate: NSPredicate.manualGlucoseNotYetUploadedToTidepool,
- key: "date",
- ascending: false
- )
- return try await context.perform {
- guard let fetchedResults = results as? [GlucoseStored] else {
- throw CoreDataError.fetchError(function: #function, file: #file)
- }
- return fetchedResults.map { result in
- BloodGlucose(
- id: result.id?.uuidString ?? UUID().uuidString,
- sgv: Int(result.glucose),
- direction: BloodGlucose.Direction(from: result.direction ?? ""),
- date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
- dateString: result.date ?? Date(),
- unfiltered: Decimal(result.glucose),
- filtered: Decimal(result.glucose),
- noise: nil,
- glucose: Int(result.glucose)
- )
- }.map { $0.convertStoredGlucoseSample(isManualGlucose: true) }
- }
- }
- func deleteGlucose(_ treatmentObjectID: NSManagedObjectID) async {
- // Use injected context if available, otherwise create new task context
- let taskContext = context != CoreDataStack.shared.newTaskContext()
- ? context
- : CoreDataStack.shared.newTaskContext()
- taskContext.name = "deleteContext"
- taskContext.transactionAuthor = "deleteGlucose"
- await taskContext.perform {
- do {
- let result = try taskContext.existingObject(with: treatmentObjectID) as? GlucoseStored
- guard let glucoseToDelete = result else {
- debugPrint("Data Table State: \(#function) \(DebuggingIdentifiers.failed) glucose not found in core data")
- return
- }
- // Create a new DeletedGlucoseStored object and copy the properties
- if let date = glucoseToDelete.date {
- let deletedEntry = DeletedGlucoseStored(context: taskContext)
- deletedEntry.date = date
- deletedEntry.glucose = glucoseToDelete.glucose
- deletedEntry.isManualGlucoseEntry = glucoseToDelete.isManual
- }
- taskContext.delete(glucoseToDelete)
- guard taskContext.hasChanges else { return }
- try taskContext.save()
- debugPrint("\(#file) \(#function) \(DebuggingIdentifiers.succeeded) deleted glucose from core data")
- } catch {
- debugPrint(
- "\(#file) \(#function) \(DebuggingIdentifiers.failed) error while deleting glucose from core data: \(error)"
- )
- }
- }
- }
- var alarm: GlucoseAlarm? {
- /// glucose can not be older than 20 minutes due to the predicate in the fetch request
- context.performAndWait {
- do {
- guard let glucose = try fetchLatestGlucose() else { return nil }
- let glucoseValue = glucose.glucose
- if Decimal(glucoseValue) <= settingsManager.settings.lowGlucose {
- return .low
- }
- if Decimal(glucoseValue) >= settingsManager.settings.highGlucose {
- return .high
- }
- return nil
- } catch {
- debugPrint("Error fetching latest glucose: \(error)")
- return nil
- }
- }
- }
- }
- protocol GlucoseObserver {
- func glucoseDidUpdate(_ glucose: [BloodGlucose])
- }
- enum GlucoseAlarm {
- case high
- case low
- var displayName: String {
- switch self {
- case .high:
- return String(localized: "LOWALERT!", comment: "LOWALERT!")
- case .low:
- return String(localized: "HIGHALERT!", comment: "HIGHALERT!")
- }
- }
- }
|