Explorar o código

Refactor Tidepool Manager WIP

Deniz Cengiz hai 1 ano
pai
achega
a6f373904b

+ 33 - 0
FreeAPS/Sources/APS/Storage/CarbsStorage.swift

@@ -210,6 +210,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             newItem.isFPU = false
             newItem.isUploadedToNS = areFetchedFromRemote ? true : false
             newItem.isUploadedToHealth = false
+            newItem.isUploadedToTidepool = false
 
             if entry.fat != nil, entry.protein != nil, let fpuId = entry.fpuID {
                 newItem.fpuID = UUID(uuidString: fpuId)
@@ -242,6 +243,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             carbEntry.fpuID = commonFPUID
             carbEntry.isFPU = true
             carbEntry.isUploadedToNS = areFetchedFromRemote ? true : false
+            // do NOT set Health and Tidepool flags to ensure they will NOT be uploaded
             return false // return false to continue
         }
         await coredataContext.perform {
@@ -400,4 +402,35 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             }
         }
     }
+
+    func getCarbsNotYetUploadedToTidepool() async -> [CarbsEntry] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: CarbEntryStored.self,
+            onContext: coredataContext,
+            predicate: NSPredicate.carbsNotYetUploadedToHealth,
+            key: "date",
+            ascending: false
+        )
+
+        guard let carbEntries = results as? [CarbEntryStored] else {
+            return []
+        }
+
+        return await coredataContext.perform {
+            return carbEntries.map { result in
+                CarbsEntry(
+                    id: result.id?.uuidString,
+                    createdAt: result.date ?? Date(),
+                    actualDate: result.date,
+                    carbs: Decimal(result.carbs),
+                    fat: Decimal(result.fat),
+                    protein: Decimal(result.protein),
+                    note: result.note,
+                    enteredBy: "Trio",
+                    isFPU: result.isFPU,
+                    fpuID: result.fpuID?.uuidString
+                )
+            }
+        }
+    }
 }

+ 1 - 0
FreeAPS/Sources/APS/Storage/GlucoseStorage.swift

@@ -83,6 +83,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                         glucoseEntry.direction = entry.direction?.rawValue
                         glucoseEntry.isUploadedToNS = false /// the value is not uploaded to NS (yet)
                         glucoseEntry.isUploadedToHealth = false /// the value is not uploaded to Health (yet)
+                        glucoseEntry.isUploadedToTidepool = false /// the value is not uploaded to Tidepool (yet)
                         return false // Continue processing
                     }
                 )

+ 39 - 0
FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift

@@ -22,6 +22,7 @@ protocol PumpHistoryStorage {
     func recent() -> [PumpHistoryEvent]
     func getPumpHistoryNotYetUploadedToNightscout() async -> [NightscoutTreatment]
     func getPumpHistoryNotYetUploadedToHealth() async -> [PumpHistoryEvent]
+    func getPumpHistoryNotYetUploadedToTidepool() async -> [PumpHistoryEvent]
     func deleteInsulin(at date: Date)
 }
 
@@ -84,6 +85,7 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                                         existingEvent.bolus?.isSMB = dose.automatic ?? true
                                         existingEvent.isUploadedToNS = false
                                         existingEvent.isUploadedToHealth = false
+                                        existingEvent.isUploadedToTidepool = false
 
                                         print("Updated existing event with smaller value: \(amount)")
                                     }
@@ -98,6 +100,7 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                         newPumpEvent.type = PumpEvent.bolus.rawValue
                         newPumpEvent.isUploadedToNS = false
                         newPumpEvent.isUploadedToHealth = false
+                        newPumpEvent.isUploadedToTidepool = false
 
                         let newBolusEntry = BolusStored(context: self.context)
                         newBolusEntry.pumpEvent = newPumpEvent
@@ -471,4 +474,40 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
             }.compactMap { $0 }
         }
     }
+
+    func getPumpHistoryNotYetUploadedToTidepool() async -> [PumpHistoryEvent] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: PumpEventStored.self,
+            onContext: context,
+            predicate: NSPredicate.pumpEventsNotYetUploadedToHealth,
+            key: "timestamp",
+            ascending: false,
+            fetchLimit: 288
+        )
+
+        guard let fetchedPumpEvents = results as? [PumpEventStored] else { return [] }
+
+        return await context.perform {
+            fetchedPumpEvents.map { event in
+                switch event.type {
+                case PumpEvent.bolus.rawValue:
+                    return PumpHistoryEvent(
+                        id: event.id ?? UUID().uuidString,
+                        type: .bolus,
+                        timestamp: event.timestamp ?? Date(),
+                        amount: event.bolus?.amount as Decimal?
+                    )
+                case PumpEvent.tempBasal.rawValue:
+                    return PumpHistoryEvent(
+                        id: event.id ?? UUID().uuidString,
+                        type: .tempBasal,
+                        timestamp: event.timestamp ?? Date(),
+                        amount: event.tempBasal?.rate as Decimal?
+                    )
+                default:
+                    return nil
+                }
+            }.compactMap { $0 }
+        }
+    }
 }

+ 1 - 0
FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -249,6 +249,7 @@ extension DataTable {
                 newItem.isManual = true
                 newItem.isUploadedToNS = false
                 newItem.isUploadedToHealth = false
+                newItem.isUploadedToTidepool = false
 
                 do {
                     guard self.coredataContext.hasChanges else { return }

+ 1 - 1
FreeAPS/Sources/Modules/Settings/SettingsStateModel.swift

@@ -99,6 +99,6 @@ extension Settings.StateModel: ServiceOnboardingDelegate {
 extension Settings.StateModel: CompletionDelegate {
     func completionNotifyingDidComplete(_: CompletionNotifying) {
         setupTidepool = false
-        provider.tidepoolManager.forceUploadData(device: fetchCgmManager.cgmManager?.cgmManagerStatus.device)
+        provider.tidepoolManager.forceTidepoolDataUpload(device: fetchCgmManager.cgmManager?.cgmManagerStatus.device)
     }
 }

+ 158 - 60
FreeAPS/Sources/Services/Network/TidepoolManager.swift

@@ -1,4 +1,5 @@
 import Combine
+import CoreData
 import Foundation
 import HealthKit
 import LoopKit
@@ -9,16 +10,15 @@ protocol TidepoolManager {
     func addTidepoolService(service: Service)
     func getTidepoolServiceUI() -> ServiceUI?
     func getTidepoolPluginHost() -> PluginHost?
+    func uploadCarbs() async
     func deleteCarbs(at date: Date, isFPU: Bool?, fpuID: String?, syncID: String)
+    func uploadInsulin() async
     func deleteInsulin(at date: Date)
-//    func uploadStatus()
     func uploadGlucose(device: HKDevice?) async
-    func forceUploadData(device: HKDevice?)
-//    func uploadPreferences(_ preferences: Preferences)
-//    func uploadProfileAndSettings(_: Bool)
+    func forceTidepoolDataUpload(device: HKDevice?)
 }
 
-final class BaseTidepoolManager: TidepoolManager, Injectable {
+final class BaseTidepoolManager: TidepoolManager, Injectable, CarbsStoredDelegate, PumpHistoryDelegate {
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var pluginManager: PluginManager!
     @Injected() private var glucoseStorage: GlucoseStorage!
@@ -37,11 +37,29 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
         }
     }
 
+    private var backgroundContext = CoreDataStack.shared.newTaskContext()
+
+    func carbsStorageHasUpdatedCarbs(_: BaseCarbsStorage) {
+        Task.detached { [weak self] in
+            guard let self = self else { return }
+            await self.uploadCarbs()
+        }
+    }
+
+    func pumpHistoryHasUpdated(_: BasePumpHistoryStorage) {
+        Task.detached { [weak self] in
+            guard let self = self else { return }
+            await self.uploadInsulin()
+        }
+    }
+
     @PersistedProperty(key: "TidepoolState") var rawTidepoolManager: Service.RawValue?
 
     init(resolver: Resolver) {
         injectServices(resolver)
         loadTidepoolManager()
+        pumpHistoryStorage.delegate = self
+        carbsStorage.delegate = self
         subscribe()
     }
 
@@ -85,8 +103,6 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
     }
 
     private func subscribe() {
-        broadcaster.register(PumpHistoryObserver.self, observer: self)
-        broadcaster.register(CarbsObserver.self, observer: self)
         broadcaster.register(TempTargetsObserver.self, observer: self)
     }
 
@@ -94,9 +110,11 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
         nil
     }
 
-    func uploadCarbs() {
-        let carbs: [CarbsEntry] = carbsStorage.recent()
+    func uploadCarbs() async {
+        uploadCarbs(await carbsStorage.getCarbsNotYetUploadedToHealth())
+    }
 
+    func uploadCarbs(_ carbs: [CarbsEntry]) {
         guard !carbs.isEmpty, let tidepoolService = self.tidepoolService else { return }
 
         processQueue.async {
@@ -111,12 +129,38 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
                         debug(.nightscout, "Error synchronizing carbs data: \(String(describing: error))")
                     case .success:
                         debug(.nightscout, "Success synchronizing carbs data:")
+                        // After successful upload, update the isUploadedToTidepool flag in Core Data
+                        Task {
+                            await self.updateCarbsAsUploaded(carbs)
+                        }
                     }
                 }
             }
         }
     }
 
+    private func updateCarbsAsUploaded(_ carbs: [CarbsEntry]) async {
+        await backgroundContext.perform {
+            let ids = carbs.map(\.id) as NSArray
+            let fetchRequest: NSFetchRequest<CarbEntryStored> = CarbEntryStored.fetchRequest()
+            fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
+
+            do {
+                let results = try self.backgroundContext.fetch(fetchRequest)
+                for result in results {
+                    result.isUploadedToHealth = true
+                }
+
+                guard self.backgroundContext.hasChanges else { return }
+                try self.backgroundContext.save()
+            } catch let error as NSError {
+                debugPrint(
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToHealth: \(error.userInfo)"
+                )
+            }
+        }
+    }
+
     func deleteCarbs(at date: Date, isFPU: Bool?, fpuID: String?, syncID _: String) {
         guard let tidepoolService = self.tidepoolService else { return }
 
@@ -146,39 +190,11 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
         }
     }
 
-    func deleteInsulin(at d: Date) {
-        let allValues = storage.retrieve(OpenAPS.Monitor.pumpHistory, as: [PumpHistoryEvent].self) ?? []
-
-        guard !allValues.isEmpty, let tidepoolService = self.tidepoolService else { return }
-
-        var doseDataToDelete: [DoseEntry] = []
-
-        guard let entry = allValues.first(where: { $0.timestamp == d }) else {
-            return
-        }
-        doseDataToDelete
-            .append(DoseEntry(
-                type: .bolus,
-                startDate: entry.timestamp,
-                value: Double(entry.amount!),
-                unit: .units,
-                syncIdentifier: entry.id
-            ))
-
-        processQueue.async {
-            tidepoolService.uploadDoseData(created: [], deleted: doseDataToDelete) { result in
-                switch result {
-                case let .failure(error):
-                    debug(.nightscout, "Error synchronizing Dose delete data: \(String(describing: error))")
-                case .success:
-                    debug(.nightscout, "Success synchronizing Dose delete data:")
-                }
-            }
-        }
+    func uploadInsulin() async {
+        uploadDose(await pumpHistoryStorage.getPumpHistoryNotYetUploadedToTidepool())
     }
 
-    func uploadDose() {
-        let events = pumpHistoryStorage.recent()
+    func uploadDose(_ events: [PumpHistoryEvent]) {
         guard !events.isEmpty, let tidepoolService = self.tidepoolService else { return }
 
         let eventsBasal = events.filter { $0.type == .tempBasal || $0.type == .tempBasalDuration }
@@ -203,7 +219,6 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
                         unit: last.unit,
                         deliveredUnits: value,
                         syncIdentifier: last.syncIdentifier,
-                        // scheduledBasalRate: last.scheduledBasalRate,
                         insulinType: last.insulinType,
                         automatic: last.automatic,
                         manuallyEntered: last.manuallyEntered
@@ -215,7 +230,7 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
                     value: 0.0,
                     unit: .units,
                     syncIdentifier: event.id,
-                    scheduledBasalRate: HKQuantity(unit: .internationalUnitsPerHour, doubleValue: Double(event.rate!)),
+                    scheduledBasalRate: HKQuantity(unit: .internationalUnitsPerHour, doubleValue: Double(event.amount!)),
                     insulinType: nil,
                     automatic: true,
                     manuallyEntered: false,
@@ -263,8 +278,8 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
                     syncIdentifier: event.id,
                     scheduledBasalRate: nil,
                     insulinType: nil,
-                    automatic: true,
-                    manuallyEntered: false
+                    automatic: event.isSMB ?? true,
+                    manuallyEntered: event.isExternal ?? false
                 )
             default: return nil
             }
@@ -303,6 +318,14 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
                     debug(.nightscout, "Error synchronizing Dose data: \(String(describing: error))")
                 case .success:
                     debug(.nightscout, "Success synchronizing Dose data:")
+                    // After successful upload, update the isUploadedToTidepool flag in Core Data
+                    Task {
+                        let insulinEvents = events
+                            .filter {
+                                $0.type == .tempBasal || $0.type == .tempBasalDuration || $0.type == .bolus
+                            }
+                        await self.updateInsulinAsUploaded(insulinEvents)
+                    }
                 }
             }
 
@@ -312,6 +335,67 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
                     debug(.nightscout, "Error synchronizing Pump Event data: \(String(describing: error))")
                 case .success:
                     debug(.nightscout, "Success synchronizing Pump Event data:")
+                    // After successful upload, update the isUploadedToTidepool flag in Core Data
+                    Task {
+                        let pumpEventType = events.map({ $0.type.mapEventTypeToPumpEventType()
+                        })
+                        let pumpEvents = events.filter { _ in pumpEventType.contains(pumpEventType) }
+
+                        await self.updateInsulinAsUploaded(pumpEvents)
+                    }
+                }
+            }
+        }
+    }
+
+    private func updateInsulinAsUploaded(_ insulin: [PumpHistoryEvent]) async {
+        await backgroundContext.perform {
+            let ids = insulin.map(\.id) as NSArray
+            let fetchRequest: NSFetchRequest<PumpEventStored> = PumpEventStored.fetchRequest()
+            fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
+
+            do {
+                let results = try self.backgroundContext.fetch(fetchRequest)
+                for result in results {
+                    result.isUploadedToHealth = true
+                }
+
+                guard self.backgroundContext.hasChanges else { return }
+                try self.backgroundContext.save()
+            } catch let error as NSError {
+                debugPrint(
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToHealth: \(error.userInfo)"
+                )
+            }
+        }
+    }
+
+    func deleteInsulin(at d: Date) {
+        let allValues = storage.retrieve(OpenAPS.Monitor.pumpHistory, as: [PumpHistoryEvent].self) ?? []
+
+        guard !allValues.isEmpty, let tidepoolService = self.tidepoolService else { return }
+
+        var doseDataToDelete: [DoseEntry] = []
+
+        guard let entry = allValues.first(where: { $0.timestamp == d }) else {
+            return
+        }
+        doseDataToDelete
+            .append(DoseEntry(
+                type: .bolus,
+                startDate: entry.timestamp,
+                value: Double(entry.amount!),
+                unit: .units,
+                syncIdentifier: entry.id
+            ))
+
+        processQueue.async {
+            tidepoolService.uploadDoseData(created: [], deleted: doseDataToDelete) { result in
+                switch result {
+                case let .failure(error):
+                    debug(.nightscout, "Error synchronizing Dose delete data: \(String(describing: error))")
+                case .success:
+                    debug(.nightscout, "Success synchronizing Dose delete data:")
                 }
             }
         }
@@ -336,43 +420,57 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
                     switch result {
                     case .success:
                         debug(.nightscout, "Success synchronizing glucose data:")
+                        // After successful upload, update the isUploadedToTidepool flag in Core Data
+                        Task {
+                            await self.updateGlucoseAsUploaded(glucose)
+                        }
                     case let .failure(error):
                         debug(.nightscout, "Error synchronizing glucose data: \(String(describing: error))")
-                        // self.uploadFailed(key)
                     }
                 }
             }
         }
     }
 
+    private func updateGlucoseAsUploaded(_ glucose: [BloodGlucose]) async {
+        await backgroundContext.perform {
+            let ids = glucose.map(\.id) as NSArray
+            let fetchRequest: NSFetchRequest<GlucoseStored> = GlucoseStored.fetchRequest()
+            fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
+
+            do {
+                let results = try self.backgroundContext.fetch(fetchRequest)
+                for result in results {
+                    result.isUploadedToHealth = true
+                }
+
+                guard self.backgroundContext.hasChanges else { return }
+                try self.backgroundContext.save()
+            } catch let error as NSError {
+                debugPrint(
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToHealth: \(error.userInfo)"
+                )
+            }
+        }
+    }
+
     /// force to uploads all data in Tidepool Service
-    func forceUploadData(device: HKDevice?) {
+    func forceTidepoolDataUpload(device: HKDevice?) {
         Task {
-            uploadDose()
-            uploadCarbs()
+            await uploadInsulin()
+            await uploadCarbs()
             await uploadGlucose(device: device)
         }
     }
 }
 
-extension BaseTidepoolManager: PumpHistoryObserver {
-    func pumpHistoryDidUpdate(_: [PumpHistoryEvent]) {
-        uploadDose()
-    }
-}
-
-extension BaseTidepoolManager: CarbsObserver {
-    func carbsDidUpdate(_: [CarbsEntry]) {
-        uploadCarbs()
-    }
-}
-
 extension BaseTidepoolManager: TempTargetsObserver {
     func tempTargetsDidUpdate(_: [TempTarget]) {}
 }
 
 extension BaseTidepoolManager: ServiceDelegate {
     var hostIdentifier: String {
+        // TODO: shouldn't this rather be `org.nightscout.Trio` ?
         "com.loopkit.Loop" // To check
     }
 

+ 1 - 0
Model/Classes+Properties/CarbEntryStored+CoreDataProperties.swift

@@ -14,6 +14,7 @@ public extension CarbEntryStored {
     @NSManaged var isFPU: Bool
     @NSManaged var isUploadedToNS: Bool
     @NSManaged var isUploadedToHealth: Bool
+    @NSManaged var isUploadedToTidepool: Bool
     @NSManaged var note: String?
     @NSManaged var protein: Double
 }

+ 1 - 0
Model/Classes+Properties/GlucoseStored+CoreDataProperties.swift

@@ -13,6 +13,7 @@ public extension GlucoseStored {
     @NSManaged var isManual: Bool
     @NSManaged var isUploadedToNS: Bool
     @NSManaged var isUploadedToHealth: Bool
+    @NSManaged var isUploadedToTidepool: Bool
 }
 
 extension GlucoseStored: Identifiable {}

+ 1 - 0
Model/Classes+Properties/PumpEventStored+CoreDataProperties.swift

@@ -9,6 +9,7 @@ public extension PumpEventStored {
     @NSManaged var id: String?
     @NSManaged var isUploadedToNS: Bool
     @NSManaged var isUploadedToHealth: Bool
+    @NSManaged var isUploadedToTidepool: Bool
     @NSManaged var note: String?
     @NSManaged var timestamp: Date?
     @NSManaged var type: String?

+ 9 - 0
Model/Helper/CarbEntryStored+helper.swift

@@ -31,6 +31,15 @@ extension NSPredicate {
         )
     }
 
+    static var carbsNotYetUploadedToTidepool: NSPredicate {
+        let date = Date.oneDayAgo
+        return NSPredicate(
+            format: "date >= %@ AND isUploadedToTidepool == %@",
+            date as NSDate,
+            false as NSNumber
+        )
+    }
+
     static var fpusNotYetUploadedToNightscout: NSPredicate {
         let date = Date.oneDayAgo
         return NSPredicate(

+ 5 - 0
Model/Helper/GlucoseStored+helper.swift

@@ -75,6 +75,11 @@ extension NSPredicate {
         return NSPredicate(format: "date >= %@ AND isUploadedToHealth == %@", date as NSDate, false as NSNumber)
     }
 
+    static var glucoseNotYetUploadedToTidepool: NSPredicate {
+        let date = Date.oneDayAgo
+        return NSPredicate(format: "date >= %@ AND isUploadedToTidepool == %@", date as NSDate, false as NSNumber)
+    }
+
     static var manualGlucoseNotYetUploadedToNightscout: NSPredicate {
         let date = Date.oneDayAgo
         return NSPredicate(

+ 5 - 0
Model/Helper/PumpEvent+helper.swift

@@ -87,6 +87,11 @@ extension NSPredicate {
         let date = Date.oneDayAgo
         return NSPredicate(format: "timestamp >= %@ AND isUploadedToHealth == %@", date as NSDate, false as NSNumber)
     }
+
+    static var pumpEventsNotYetUploadedToTidepool: NSPredicate {
+        let date = Date.oneDayAgo
+        return NSPredicate(format: "timestamp >= %@ AND isUploadedToTidepool == %@", date as NSDate, false as NSNumber)
+    }
 }
 
 // Declare helper structs ("data transfer objects" = DTO) to utilize parsing a flattened pump history

+ 4 - 1
Model/TrioCoreDataPersistentContainer.xcdatamodeld/TrioCoreDataPersistentContainer.xcdatamodel/contents

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23222.3" systemVersion="23G93" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
+<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22757" systemVersion="23G93" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
     <entity name="BolusStored" representedClassName="BolusStored" syncable="YES">
         <attribute name="amount" optional="YES" attributeType="Decimal" defaultValueString="0"/>
         <attribute name="isExternal" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
@@ -15,6 +15,7 @@
         <attribute name="isFPU" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
         <attribute name="isUploadedToHealth" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
         <attribute name="isUploadedToNS" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
+        <attribute name="isUploadedToTidepool" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
         <attribute name="note" optional="YES" attributeType="String"/>
         <attribute name="protein" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
         <fetchIndex name="byDate">
@@ -47,6 +48,7 @@
         <attribute name="isManual" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
         <attribute name="isUploadedToHealth" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
         <attribute name="isUploadedToNS" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
+        <attribute name="isUploadedToTidepool" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
         <fetchIndex name="byDate">
             <fetchIndexElement property="date" type="Binary" order="ascending"/>
         </fetchIndex>
@@ -168,6 +170,7 @@
         <attribute name="id" optional="YES" attributeType="String"/>
         <attribute name="isUploadedToHealth" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
         <attribute name="isUploadedToNS" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
+        <attribute name="isUploadedToTidepool" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
         <attribute name="note" optional="YES" attributeType="String"/>
         <attribute name="timestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
         <attribute name="type" optional="YES" attributeType="String"/>