diff --git a/FreeAPS.xcodeproj/project.pbxproj b/FreeAPS.xcodeproj/project.pbxproj index 7f12b9aea..9d8b341c0 100644 --- a/FreeAPS.xcodeproj/project.pbxproj +++ b/FreeAPS.xcodeproj/project.pbxproj @@ -304,7 +304,8 @@ E013D872273AC6FE0014109C /* GlucoseSimulatorSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E013D871273AC6FE0014109C /* GlucoseSimulatorSource.swift */; }; E06B911A275B5EEA003C04B6 /* Array+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E06B9119275B5EEA003C04B6 /* Array+Extension.swift */; }; E0CC2C5C275B9F0F00A7BC71 /* HealthKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E0CC2C5B275B9DAE00A7BC71 /* HealthKit.framework */; }; - E0D4F80527513ECF00BDF1FE /* HealthKitSample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0D4F80427513ECF00BDF1FE /* HealthKitSample.swift */; }; + E0D4F80527513ECF00BDF1FE /* HealthKitSamples.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0D4F80427513ECF00BDF1FE /* HealthKitSamples.swift */; }; + E0E9DB9727F051E700614A3F /* CarbsSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0E9DB9627F051E700614A3F /* CarbsSource.swift */; }; E13B7DAB2A435F57066AF02E /* TargetsEditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36F58DDD71F0E795464FA3F0 /* TargetsEditorStateModel.swift */; }; E25073BC86C11C3D6A42F5AC /* CalibrationsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47DFCE895C930F784EF11843 /* CalibrationsStateModel.swift */; }; E39E418C56A5A46B61D960EE /* ConfigEditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D5B4F8B4194BB7E260EF251 /* ConfigEditorStateModel.swift */; }; @@ -729,7 +730,8 @@ E013D871273AC6FE0014109C /* GlucoseSimulatorSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseSimulatorSource.swift; sourceTree = ""; }; E06B9119275B5EEA003C04B6 /* Array+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extension.swift"; sourceTree = ""; }; E0CC2C5B275B9DAE00A7BC71 /* HealthKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HealthKit.framework; path = System/Library/Frameworks/HealthKit.framework; sourceTree = SDKROOT; }; - E0D4F80427513ECF00BDF1FE /* HealthKitSample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitSample.swift; sourceTree = ""; }; + E0D4F80427513ECF00BDF1FE /* HealthKitSamples.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitSamples.swift; sourceTree = ""; }; + E0E9DB9627F051E700614A3F /* CarbsSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbsSource.swift; sourceTree = ""; }; E26904AACA8D9C15D229D675 /* SnoozeStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SnoozeStateModel.swift; sourceTree = ""; }; E2EBA7C03C26FCC67E16D798 /* LibreConfigProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LibreConfigProvider.swift; sourceTree = ""; }; E625985B47742D498CB1681A /* NotificationsConfigProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NotificationsConfigProvider.swift; sourceTree = ""; }; @@ -1131,6 +1133,7 @@ 38A43597262E0E4900E80935 /* FetchAnnouncementsManager.swift */, 38DAB289260D349500F74C1A /* FetchGlucoseManager.swift */, 38192E06261BA9960094D973 /* FetchTreatmentsManager.swift */, + E0E9DB9527F051C300614A3F /* Carbs */, 3856933F270B57A00002C50D /* CGM */, 38A504F625DDA0E200C5B9E8 /* Extensions */, 388E5A5825B6F0070019842D /* OpenAPS */, @@ -1308,7 +1311,7 @@ 38A0364125ED069400FCBB52 /* TempBasal.swift */, 3871F39B25ED892B0013ECB5 /* TempTarget.swift */, 3811DE8E25C9D80400A708ED /* User.swift */, - E0D4F80427513ECF00BDF1FE /* HealthKitSample.swift */, + E0D4F80427513ECF00BDF1FE /* HealthKitSamples.swift */, ); path = Models; sourceTree = ""; @@ -1802,6 +1805,14 @@ path = Assemblies; sourceTree = ""; }; + E0E9DB9527F051C300614A3F /* Carbs */ = { + isa = PBXGroup; + children = ( + E0E9DB9627F051E700614A3F /* CarbsSource.swift */, + ); + path = Carbs; + sourceTree = ""; + }; E42231DBF0DBE2B4B92D1B15 /* CREditor */ = { isa = PBXGroup; children = ( @@ -1945,11 +1956,11 @@ buildConfigurationList = 388E596725AD948E0019842D /* Build configuration list for PBXNativeTarget "FreeAPS" */; buildPhases = ( 3811DEF525CA169200A708ED /* Swiftformat */, - 388E595425AD948C0019842D /* Sources */, 388E595525AD948C0019842D /* Frameworks */, 388E595625AD948C0019842D /* Resources */, 3821ECD025DC703C00BC42AD /* Embed Frameworks */, 38E8753D27554D5900975559 /* Embed Watch Content */, + 388E595425AD948C0019842D /* Sources */, ); buildRules = ( ); @@ -2361,7 +2372,7 @@ D2165E9D78EFF692C1DED1C6 /* AddTempTargetDataFlow.swift in Sources */, 38E4451E274DB04600EC9A94 /* AppDelegate.swift in Sources */, 5BFA1C2208114643B77F8CEB /* AddTempTargetProvider.swift in Sources */, - E0D4F80527513ECF00BDF1FE /* HealthKitSample.swift in Sources */, + E0D4F80527513ECF00BDF1FE /* HealthKitSamples.swift in Sources */, 919DBD08F13BAFB180DF6F47 /* AddTempTargetStateModel.swift in Sources */, 8BC2F5A29AD1ED08AC0EE013 /* AddTempTargetRootView.swift in Sources */, 38A00B1F25FC00F7006BC0B0 /* Autotune.swift in Sources */, @@ -2402,6 +2413,7 @@ B7C465E9472624D8A2BE2A6A /* CalibrationsDataFlow.swift in Sources */, 320D030F724170A637F06D50 /* CalibrationsProvider.swift in Sources */, E25073BC86C11C3D6A42F5AC /* CalibrationsStateModel.swift in Sources */, + E0E9DB9727F051E700614A3F /* CarbsSource.swift in Sources */, BA90041DC8991147E5C8C3AA /* CalibrationsRootView.swift in Sources */, E3A08AAE59538BC8A8ABE477 /* NotificationsConfigDataFlow.swift in Sources */, 0F7A65FBD2CD8D6477ED4539 /* NotificationsConfigProvider.swift in Sources */, diff --git a/FreeAPS.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/FreeAPS.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000..f9b0d7c5e --- /dev/null +++ b/FreeAPS.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/FreeAPS/Sources/APS/APSManager.swift b/FreeAPS/Sources/APS/APSManager.swift index 746d3b961..6b6d1073b 100644 --- a/FreeAPS/Sources/APS/APSManager.swift +++ b/FreeAPS/Sources/APS/APSManager.swift @@ -61,6 +61,7 @@ final class BaseAPSManager: APSManager, Injectable { @Injected() private var deviceDataManager: DeviceDataManager! @Injected() private var nightscout: NightscoutManager! @Injected() private var settingsManager: SettingsManager! + @Injected() private var healthKitManager: HealthKitManager! @Injected() private var broadcaster: Broadcaster! @Persisted(key: "lastAutotuneDate") private var lastAutotuneDate = Date() @Persisted(key: "lastLoopDate") var lastLoopDate: Date = .distantPast { @@ -178,6 +179,9 @@ final class BaseAPSManager: APSManager, Injectable { } else { self.loopCompleted() } + // upload insulin to healthKit (from pump history) + let events = self.pumpHistoryStorage.recent() + self.healthKitManager.saveIfNeeded(pumpEvents: events) } receiveValue: {} .store(in: &lifetime) } diff --git a/FreeAPS/Sources/APS/Carbs/CarbsSource.swift b/FreeAPS/Sources/APS/Carbs/CarbsSource.swift new file mode 100644 index 000000000..ecaf3fb54 --- /dev/null +++ b/FreeAPS/Sources/APS/Carbs/CarbsSource.swift @@ -0,0 +1,5 @@ +import Combine + +protocol CarbsSource: SourceInfoProvider { + func fetchCarbs() -> AnyPublisher<[CarbsEntry], Never> +} diff --git a/FreeAPS/Sources/APS/FetchTreatmentsManager.swift b/FreeAPS/Sources/APS/FetchTreatmentsManager.swift index 81be5a6b9..9b3fe1b0c 100644 --- a/FreeAPS/Sources/APS/FetchTreatmentsManager.swift +++ b/FreeAPS/Sources/APS/FetchTreatmentsManager.swift @@ -8,6 +8,7 @@ protocol FetchTreatmentsManager {} final class BaseFetchTreatmentsManager: FetchTreatmentsManager, Injectable { private let processQueue = DispatchQueue(label: "BaseFetchTreatmentsManager.processQueue") @Injected() var nightscoutManager: NightscoutManager! + @Injected() var healthKitManager: HealthKitManager! @Injected() var tempTargetsStorage: TempTargetsStorage! @Injected() var carbsStorage: CarbsStorage! @@ -22,15 +23,17 @@ final class BaseFetchTreatmentsManager: FetchTreatmentsManager, Injectable { private func subscribe() { timer.publisher .receive(on: processQueue) - .flatMap { _ -> AnyPublisher<([CarbsEntry], [TempTarget]), Never> in + .flatMap { _ -> AnyPublisher<([CarbsEntry], [CarbsEntry], [TempTarget]), Never> in debug(.nightscout, "FetchTreatmentsManager heartbeat") debug(.nightscout, "Start fetching carbs and temptargets") - return Publishers.CombineLatest( + return Publishers.CombineLatest3( self.nightscoutManager.fetchCarbs(), + self.healthKitManager.fetchCarbs(), self.nightscoutManager.fetchTempTargets() ).eraseToAnyPublisher() } - .sink { carbs, targets in + .sink { carbsFromNS, carbsFromAH, targets in + let carbs = carbsFromAH + carbsFromNS let filteredCarbs = carbs.filter { !($0.enteredBy?.contains(CarbsEntry.manual) ?? false) } if filteredCarbs.isNotEmpty { self.carbsStorage.storeCarbs(filteredCarbs) diff --git a/FreeAPS/Sources/APS/Storage/CarbsStorage.swift b/FreeAPS/Sources/APS/Storage/CarbsStorage.swift index 16c23f10d..78ebeac2d 100644 --- a/FreeAPS/Sources/APS/Storage/CarbsStorage.swift +++ b/FreeAPS/Sources/APS/Storage/CarbsStorage.swift @@ -65,7 +65,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable { func nightscoutTretmentsNotUploaded() -> [NigtscoutTreatment] { let uploaded = storage.retrieve(OpenAPS.Nightscout.uploadedPumphistory, as: [NigtscoutTreatment].self) ?? [] - let eventsManual = recent().filter { $0.enteredBy == CarbsEntry.manual } + let eventsManual = recent().filter { $0.enteredBy == CarbsEntry.manual || $0.enteredBy == CarbsEntry.applehealth } let treatments = eventsManual.map { NigtscoutTreatment( duration: nil, diff --git a/FreeAPS/Sources/Application/FreeAPSApp.swift b/FreeAPS/Sources/Application/FreeAPSApp.swift index 395b5dcae..aba1c78b0 100644 --- a/FreeAPS/Sources/Application/FreeAPSApp.swift +++ b/FreeAPS/Sources/Application/FreeAPSApp.swift @@ -49,8 +49,8 @@ import Swinject var body: some Scene { WindowGroup { - Main.RootView(resolver: resolver) - .onOpenURL(perform: handleURL) + rootView + .animation(.easeIn(duration: 0.75), value: self.loadingIsEnded) } .onChange(of: scenePhase) { newScenePhase in debug(.default, "APPLICATION PHASE: \(newScenePhase)") @@ -68,6 +68,7 @@ import Swinject } // Migration is temporary disabled + @ViewBuilder private var rootView: some View { if !loadingIsEnded { Screen.migration.view(resolver: resolver) diff --git a/FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings b/FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings index 48740e8ac..81a5e7fe1 100644 --- a/FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings +++ b/FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings @@ -943,11 +943,8 @@ Enact a temp Basal or a temp target */ /* */ "Connect to Apple Health" = "Connect to Apple Health"; -/* Show when have not permissions for writing to Health */ -"For write data to Apple Health you must give permissions in Settings > Health > Data Access" = "For write data to Apple Health you must give permissions in Settings > Health > Data Access"; - /* */ -"After you create glucose records in the Health app, please open FreeAPS X to help us guaranteed transfer changed data" = "After you create glucose records in the Health app, please open FreeAPS X to help us guaranteed transfer changed data"; +"After you create records in the Health app, please open FreeAPS X to help us guaranteed transfer changed data" = "After you create records in the Health app, please open FreeAPS X to help us guaranteed transfer changed data"; /* -------------------------------------------- */ /* diff --git a/FreeAPS/Sources/Localizations/Main/ru.lproj/Localizable.strings b/FreeAPS/Sources/Localizations/Main/ru.lproj/Localizable.strings index 2daa2f8da..72d19769f 100644 --- a/FreeAPS/Sources/Localizations/Main/ru.lproj/Localizable.strings +++ b/FreeAPS/Sources/Localizations/Main/ru.lproj/Localizable.strings @@ -943,11 +943,8 @@ Enact a temp Basal or a temp target */ /* */ "Connect to Apple Health" = "Подключить к Apple Health"; -/* Show when have not permissions for writing to Health */ -"For write data to Apple Health you must give permissions in Settings > Health > Data Access" = "Чтобы записывать данные в Apple Health вам необходимо дать соответствующие разрешения, перейдя к меню Настройки > Здоровье > Доступ к данным"; - /* */ -"After you create glucose records in the Health app, please open FreeAPS X to help us guaranteed transfer changed data" = "После ручного создания записей о глюкозы в программе Здоровье пожалуйста откройте FreeAPS X, чтобы помочь нам гарантированно загрузить измененные данные"; +"After you create records in the Health app, please open FreeAPS X to help us guaranteed transfer changed data" = "После ручного создания записей в программе Здоровье пожалуйста откройте FreeAPS X, чтобы помочь нам гарантированно загрузить измененные данные"; /* -------------------------------------------- diff --git a/FreeAPS/Sources/Models/CarbsEntry.swift b/FreeAPS/Sources/Models/CarbsEntry.swift index dbcbc237f..aa8333dbb 100644 --- a/FreeAPS/Sources/Models/CarbsEntry.swift +++ b/FreeAPS/Sources/Models/CarbsEntry.swift @@ -1,11 +1,13 @@ import Foundation struct CarbsEntry: JSON, Equatable, Hashable { + var id = UUID().uuidString let createdAt: Date let carbs: Decimal let enteredBy: String? static let manual = "freeaps-x" + static let applehealth = "applehealth" static func == (lhs: CarbsEntry, rhs: CarbsEntry) -> Bool { lhs.createdAt == rhs.createdAt @@ -17,6 +19,34 @@ struct CarbsEntry: JSON, Equatable, Hashable { } extension CarbsEntry { + private enum CodingKeys: String, CodingKey { + case id = "_id" + case createdAt = "created_at" + case carbs + case enteredBy + } +} + +// MARK: CarbsEntry till 0.2.6 + +// At this version was add id propery for working with Apple Health +struct CarbsEntryTill026: JSON, Equatable, Hashable { + let createdAt: Date + let carbs: Decimal + let enteredBy: String? + + static let manual = "freeaps-x" + + static func == (lhs: CarbsEntryTill026, rhs: CarbsEntryTill026) -> Bool { + lhs.createdAt == rhs.createdAt + } + + func hash(into hasher: inout Hasher) { + hasher.combine(createdAt) + } +} + +extension CarbsEntryTill026 { private enum CodingKeys: String, CodingKey { case createdAt = "created_at" case carbs diff --git a/FreeAPS/Sources/Models/HealthKitSample.swift b/FreeAPS/Sources/Models/HealthKitSample.swift deleted file mode 100644 index 58b3f6845..000000000 --- a/FreeAPS/Sources/Models/HealthKitSample.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Foundation - -struct HealthKitSample: JSON, Hashable, Equatable { - var healthKitId: String - var date: Date - var glucose: Int - - static func == (lhs: HealthKitSample, rhs: HealthKitSample) -> Bool { - lhs.healthKitId == rhs.healthKitId - } -} - -extension HealthKitSample { - private enum CodingKeys: String, CodingKey { - case healthKitId = "healthkit_id" - case date - case glucose - } -} diff --git a/FreeAPS/Sources/Models/HealthKitSamples.swift b/FreeAPS/Sources/Models/HealthKitSamples.swift new file mode 100644 index 000000000..569dea935 --- /dev/null +++ b/FreeAPS/Sources/Models/HealthKitSamples.swift @@ -0,0 +1,41 @@ +import Foundation + +// MARK: - Blood glucose + +struct HealthKitBGSample: JSON, Hashable, Equatable { + var healthKitId: String + var date: Date + var glucose: Int + + static func == (lhs: HealthKitBGSample, rhs: HealthKitBGSample) -> Bool { + lhs.healthKitId == rhs.healthKitId + } +} + +extension HealthKitBGSample { + private enum CodingKeys: String, CodingKey { + case healthKitId = "healthkit_id" + case date + case glucose + } +} + +// MARK: - Carbs + +struct HealthKitCarbsSample: JSON, Hashable, Equatable { + var healthKitId: String + var date: Date + var carbs: Decimal + + static func == (lhs: HealthKitCarbsSample, rhs: HealthKitCarbsSample) -> Bool { + lhs.healthKitId == rhs.healthKitId + } +} + +extension HealthKitCarbsSample { + private enum CodingKeys: String, CodingKey { + case healthKitId = "healthkit_id" + case date + case carbs + } +} diff --git a/FreeAPS/Sources/Modules/DataTable/DataTableDataFlow.swift b/FreeAPS/Sources/Modules/DataTable/DataTableDataFlow.swift index 1a30a0d15..267dca125 100644 --- a/FreeAPS/Sources/Modules/DataTable/DataTableDataFlow.swift +++ b/FreeAPS/Sources/Modules/DataTable/DataTableDataFlow.swift @@ -52,7 +52,7 @@ enum DataTable { } class Treatment: Identifiable, Hashable, Equatable { - let id = UUID() + var id = UUID() let units: GlucoseUnits let type: DataType let date: Date @@ -67,6 +67,24 @@ enum DataTable { return formatter } + init( + id: UUID, + units: GlucoseUnits, + type: DataType, + date: Date, + amount: Decimal? = nil, + secondAmount: Decimal? = nil, + duration: Decimal? = nil + ) { + self.id = id + self.units = units + self.type = type + self.date = date + self.amount = amount + self.secondAmount = secondAmount + self.duration = duration + } + init( units: GlucoseUnits, type: DataType, @@ -172,6 +190,6 @@ protocol DataTableProvider: Provider { func tempTargets() -> [TempTarget] func carbs() -> [CarbsEntry] func glucose() -> [BloodGlucose] - func deleteCarbs(at date: Date) + func deleteCarbs(_ treatment: DataTable.Treatment) func deleteGlucose(id: String) } diff --git a/FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift b/FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift index 134031c3b..34f05bead 100644 --- a/FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift +++ b/FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift @@ -21,8 +21,9 @@ extension DataTable { carbsStorage.recent() } - func deleteCarbs(at date: Date) { - nightscoutManager.deleteCarbs(at: date) + func deleteCarbs(_ treatment: Treatment) { + nightscoutManager.deleteCarbs(at: treatment.date) + healthkitManager.deleteCarbs(syncID: treatment.id.uuidString) } func glucose() -> [BloodGlucose] { @@ -31,7 +32,7 @@ extension DataTable { func deleteGlucose(id: String) { glucoseStorage.removeGlucose(ids: [id]) - healthkitManager.deleteGlucise(syncID: id) + healthkitManager.deleteGlucose(syncID: id) } } } diff --git a/FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift b/FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift index 502413ed0..2f369cf57 100644 --- a/FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift +++ b/FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift @@ -24,7 +24,13 @@ extension DataTable { let units = self.settingsManager.settings.units let carbs = self.provider.carbs().map { - Treatment(units: units, type: .carbs, date: $0.createdAt, amount: $0.carbs) + Treatment( + id: UUID(uuidString: $0.id) ?? UUID(), + units: units, + type: .carbs, + date: $0.createdAt, + amount: $0.carbs + ) } let boluses = self.provider.pumpHistory() @@ -88,8 +94,8 @@ extension DataTable { } } - func deleteCarbs(at date: Date) { - provider.deleteCarbs(at: date) + func deleteCarbs(_ treatment: Treatment) { + provider.deleteCarbs(treatment) } func deleteGlucose(at index: Int) { diff --git a/FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift b/FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift index 1c5bb6cfc..04c185107 100644 --- a/FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift +++ b/FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift @@ -90,7 +90,9 @@ extension DataTable { message: Text(item.amountText), primaryButton: .destructive( Text("Delete"), - action: { state.deleteCarbs(at: item.date) } + action: { + state.deleteCarbs(item) + } ), secondaryButton: .cancel() ) diff --git a/FreeAPS/Sources/Modules/HealthKit/HealthKitStateModel.swift b/FreeAPS/Sources/Modules/HealthKit/HealthKitStateModel.swift index fa23644b2..142ed6779 100644 --- a/FreeAPS/Sources/Modules/HealthKit/HealthKitStateModel.swift +++ b/FreeAPS/Sources/Modules/HealthKit/HealthKitStateModel.swift @@ -6,27 +6,18 @@ extension AppleHealthKit { @Injected() var healthKitManager: HealthKitManager! @Published var useAppleHealth = false - @Published var needShowInformationTextForSetPermissions = false override func subscribe() { useAppleHealth = settingsManager.settings.useAppleHealth - needShowInformationTextForSetPermissions = healthKitManager.areAllowAllPermissions - subscribeSetting(\.useAppleHealth, on: $useAppleHealth) { useAppleHealth = $0 } didSet: { [weak self] value in guard let self = self else { return } - guard value else { - self.needShowInformationTextForSetPermissions = false - return - } + guard value else { return } self.healthKitManager.requestPermission { ok, error in - DispatchQueue.main.async { - self.needShowInformationTextForSetPermissions = !self.healthKitManager.checkAvailabilitySaveBG() - } guard ok, error == nil else { warning(.service, "Permission not granted for HealthKitManager", error: error) @@ -35,8 +26,7 @@ extension AppleHealthKit { debug(.service, "Permission granted HealthKitManager") - self.healthKitManager.createObserver() - self.healthKitManager.enableBackgroundDelivery() + self.healthKitManager.configureManager() } } } diff --git a/FreeAPS/Sources/Modules/HealthKit/View/AppleHealthKitRootView.swift b/FreeAPS/Sources/Modules/HealthKit/View/AppleHealthKitRootView.swift index b7594f3d4..a0c32d059 100644 --- a/FreeAPS/Sources/Modules/HealthKit/View/AppleHealthKitRootView.swift +++ b/FreeAPS/Sources/Modules/HealthKit/View/AppleHealthKitRootView.swift @@ -13,19 +13,11 @@ extension AppleHealthKit { HStack { Image(systemName: "pencil.circle.fill") Text( - "After you create glucose records in the Health app, please open FreeAPS X to help us guaranteed transfer changed data" + "After you create records in the Health app, please open FreeAPS X to help us guaranteed transfer changed data" ) .font(.caption) } .foregroundColor(Color.secondary) - if state.needShowInformationTextForSetPermissions { - HStack { - Image(systemName: "exclamationmark.circle.fill") - Text("For write data to Apple Health you must give permissions in Settings > Health > Data Access") - .font(.caption) - } - .foregroundColor(Color.secondary) - } } } .onAppear(perform: configureView) diff --git a/FreeAPS/Sources/Modules/Migration/MigrationStateModel.swift b/FreeAPS/Sources/Modules/Migration/MigrationStateModel.swift index cddbcc906..b1afa8314 100644 --- a/FreeAPS/Sources/Modules/Migration/MigrationStateModel.swift +++ b/FreeAPS/Sources/Modules/Migration/MigrationStateModel.swift @@ -14,6 +14,7 @@ extension Migration { Publishers .getMigrationPublisher(fromMigrationManager: manager) // .migrate(startAtVersion: "0.2.6", MigrationWorkExample()) +// .migrate(startAtVersion: "0.2.6", MigrationCarbs()) .sink { _ in debug(.businessLogic, "Migration did finish") // fake pause diff --git a/FreeAPS/Sources/Modules/Migration/View/MigrationRootView.swift b/FreeAPS/Sources/Modules/Migration/View/MigrationRootView.swift index ef08afcc2..04657518a 100644 --- a/FreeAPS/Sources/Modules/Migration/View/MigrationRootView.swift +++ b/FreeAPS/Sources/Modules/Migration/View/MigrationRootView.swift @@ -17,7 +17,7 @@ extension Migration { ) .frame(width: 80, height: 80) .rotationEffect(.degrees(state.animated ? 360 : 0)) - .animation(.linear(duration: 0.7).repeatForever(autoreverses: false)) + .animation(.linear(duration: 0.7).repeatForever(autoreverses: false), value: state.animated) VStack(spacing: 0) { Text("Preparing data") .font(.title) diff --git a/FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift b/FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift index 869ea4df1..59eeead89 100644 --- a/FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift +++ b/FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift @@ -1,33 +1,37 @@ import Combine import Foundation import HealthKit +import LoopKit import Swinject -protocol HealthKitManager: GlucoseSource { - /// Check all needed permissions - /// Return false if one or more permissions are deny or not choosen - var areAllowAllPermissions: Bool { get } - /// Check availability to save data of BG type to Health store - func checkAvailabilitySaveBG() -> Bool - /// Requests user to give permissions on using HealthKit +protocol HealthKitManager: GlucoseSource, CarbsSource { + /// Requests user to give permissions to using HealthKit func requestPermission(completion: ((Bool, Error?) -> Void)?) /// Save blood glucose to Health store (dublicate of bg will ignore) func saveIfNeeded(bloodGlucose: [BloodGlucose]) - /// Create observer for data passing beetwen Health Store and FreeAPS - func createObserver() - /// Enable background delivering objects from Apple Health to FreeAPS - func enableBackgroundDelivery() + /// Save carbs to Health store (dublicate of bg will ignore) + func saveIfNeeded(carbs: [CarbsEntry]) + /// Save pumpHistoryEvents (basal and bolus event) + func saveIfNeeded(pumpEvents: [PumpHistoryEvent]) + /// Configure HealthKit manager + func configureManager() /// Delete glucose with syncID - func deleteGlucise(syncID: String) + func deleteCarbs(syncID: String) + /// Delete glucose with syncID + func deleteGlucose(syncID: String) } final class BaseHealthKitManager: HealthKitManager, Injectable { private enum Config { - // unwraped HKObjects - static var permissions: Set { Set([healthBGObject].compactMap { $0 }) } + // permissions for write and read + static var readPermissions: Set { Set([healthBGObject, healthCarbObject].compactMap { $0 }) } + static var writePermissions: Set { + Set([healthBGObject, healthCarbObject, healthInsulinObject].compactMap { $0 }) } // link to object in HealthKit static let healthBGObject = HKObjectType.quantityType(forIdentifier: .bloodGlucose) + static let healthCarbObject = HKObjectType.quantityType(forIdentifier: .dietaryCarbohydrates) + static let healthInsulinObject = HKObjectType.quantityType(forIdentifier: .insulinDelivery) // Meta-data key of FreeASPX data in HealthStore static let freeAPSMetaKey = "fromFreeAPSX" @@ -36,38 +40,51 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { @Injected() private var glucoseStorage: GlucoseStorage! @Injected() private var healthKitStore: HKHealthStore! @Injected() private var settingsManager: SettingsManager! + @Injected() private var broadcaster: Broadcaster! private let processQueue = DispatchQueue(label: "BaseHealthKitManager.processQueue") private var lifetime = Lifetime() - // BG that will be return Publisher + // BG that will be return Publisher (GlucoseSource protocol) @SyncAccess @Persisted(key: "BaseHealthKitManager.newGlucose") private var newGlucose: [BloodGlucose] = [] + // Carbs that will be return Publisher (CarbsSource protocol) + @SyncAccess @Persisted(key: "BaseHealthKitManager.newCarbs") private var newCarbs: [CarbsEntry] = [] // last anchor for HKAnchoredQuery + // BG private var lastBloodGlucoseQueryAnchor: HKQueryAnchor? { set { - persistedAnchor = try? NSKeyedArchiver.archivedData(withRootObject: newValue as Any, requiringSecureCoding: false) + persistedBGAnchor = try? NSKeyedArchiver.archivedData(withRootObject: newValue as Any, requiringSecureCoding: false) } get { - guard let data = persistedAnchor else { return nil } + guard let data = persistedBGAnchor else { return nil } return try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? HKQueryAnchor } } - @Persisted(key: "HealthKitManagerAnchor") private var persistedAnchor: Data? = nil + @Persisted(key: "HealthKitManagerAnchor") private var persistedBGAnchor: Data? = nil + // Carbs + private var lastCarbsQueryAnchor: HKQueryAnchor? { + set { + persistedCarbsAnchor = try? NSKeyedArchiver.archivedData( + withRootObject: newValue as Any, + requiringSecureCoding: false + ) + } + get { + guard let data = persistedCarbsAnchor else { return nil } + return try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? HKQueryAnchor + } + } + + @Persisted(key: "HealthKitManagerAnchor_Carbs") private var persistedCarbsAnchor: Data? = nil var isAvailableOnCurrentDevice: Bool { HKHealthStore.isHealthDataAvailable() } - var areAllowAllPermissions: Bool { - Set(Config.permissions.map { healthKitStore.authorizationStatus(for: $0) }) - .intersection([.sharingDenied, .notDetermined]) - .isEmpty - } - // NSPredicate, which use during load increment BG from Health store - private var loadBGPredicate: NSPredicate { + private var loadHealthDataPredicate: NSPredicate { // loading only daily bg let predicateByStartDate = HKQuery.predicateForSamples( withStart: Date().addingTimeInterval(-1.days.timeInterval), @@ -90,17 +107,23 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { injectServices(resolver) guard isAvailableOnCurrentDevice, Config.healthBGObject != nil else { return } + configureManager() + subscribe() + debug(.service, "HealthKitManager did create") + } + + func configureManager() { createObserver() enableBackgroundDelivery() - debug(.service, "HealthKitManager did create") + debug(.service, "HealthKitManager did configured") } - func checkAvailabilitySave(objectTypeToHealthStore: HKObjectType) -> Bool { - healthKitStore.authorizationStatus(for: objectTypeToHealthStore) == .sharingAuthorized + private func subscribe() { + broadcaster.register(CarbsObserver.self, observer: self) } - func checkAvailabilitySaveBG() -> Bool { - Config.healthBGObject.map { checkAvailabilitySave(objectTypeToHealthStore: $0) } ?? false + func checkAvailabilitySave(objectTypeToHealthStore: HKObjectType) -> Bool { + healthKitStore.authorizationStatus(for: objectTypeToHealthStore) == .sharingAuthorized } func requestPermission(completion: ((Bool, Error?) -> Void)? = nil) { @@ -108,16 +131,52 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { completion?(false, HKError.notAvailableOnCurrentDevice) return } - guard Config.permissions.isNotEmpty else { + guard Config.readPermissions.isNotEmpty, Config.writePermissions.isNotEmpty else { completion?(false, HKError.dataNotAvailable) return } - healthKitStore.requestAuthorization(toShare: Config.permissions, read: Config.permissions) { status, error in + healthKitStore.requestAuthorization(toShare: Config.writePermissions, read: Config.readPermissions) { status, error in completion?(status, error) } } + func saveIfNeeded(carbs: [CarbsEntry]) { + guard settingsManager.settings.useAppleHealth, + let sampleType = Config.healthCarbObject, + checkAvailabilitySave(objectTypeToHealthStore: sampleType), + carbs.isNotEmpty + else { return } + + func save(samples: [HKSample]) { + let sampleIDs = samples.compactMap(\.syncIdentifier) + let samplesToSave = carbs + .filter { !sampleIDs.contains($0.id) } + .filter { $0.enteredBy != CarbsEntry.applehealth } + .map { + HKQuantitySample( + type: sampleType, + quantity: HKQuantity(unit: .gram(), doubleValue: Double($0.carbs)), + start: $0.createdAt, + end: $0.createdAt, + metadata: [ + HKMetadataKeyExternalUUID: $0.id, + HKMetadataKeySyncIdentifier: $0.id, + HKMetadataKeySyncVersion: 1, + Config.freeAPSMetaKey: true + ] + ) + } + + healthKitStore.save(samplesToSave) { _, _ in } + } + + loadSamplesFromHealth(sampleType: sampleType, withIDs: carbs.map(\.id)) + .receive(on: processQueue) + .sink(receiveValue: save) + .store(in: &lifetime) + } + func saveIfNeeded(bloodGlucose: [BloodGlucose]) { guard settingsManager.settings.useAppleHealth, let sampleType = Config.healthBGObject, @@ -153,24 +212,110 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { .store(in: &lifetime) } - func createObserver() { + func saveIfNeeded(pumpEvents events: [PumpHistoryEvent]) { + guard settingsManager.settings.useAppleHealth, + let sampleType = Config.healthInsulinObject, + checkAvailabilitySave(objectTypeToHealthStore: sampleType), + events.isNotEmpty + else { return } + + loadSamplesFromHealth(sampleType: sampleType, withIDs: events.map(\.id)) + .receive(on: processQueue) + .compactMap { samples -> ([InsulinBolus], [InsulinBasal]) in + let sampleIDs = samples.compactMap(\.syncIdentifier) + let bolus = events + .filter { $0.type == .bolus && !sampleIDs.contains($0.id) } + .compactMap { event -> InsulinBolus? in + guard let amount = event.amount else { return nil } + return InsulinBolus(id: event.id, amount: amount, date: event.timestamp) + } + let basalEvents = events + .filter { $0.type == .tempBasal && !sampleIDs.contains($0.id) } + let basal = basalEvents.enumerated() + .compactMap { item -> InsulinBasal? in + let nextElementEventIndex = item.offset + 1 + guard basalEvents.count > nextElementEventIndex else { return nil } + let nextBasalEvent = basalEvents[nextElementEventIndex] + let secondsOfCurrentBasal = nextBasalEvent.timestamp.timeIntervalSince(item.element.timestamp) + let amount = Decimal(secondsOfCurrentBasal / 3600) * (item.element.rate ?? 0) + let id = String(item.element.id.dropFirst()) + guard amount > 0, + id != "" + else { return nil } + return InsulinBasal( + id: id, + amount: amount, + startDelivery: item.element.timestamp, + endDelivery: nextBasalEvent.timestamp + ) + } + return (bolus, basal) + } + .sink(receiveValue: { bolus, basal in + // save bolus + let bolusSamples = bolus + .map { + HKQuantitySample( + type: sampleType, + quantity: HKQuantity(unit: .internationalUnit(), doubleValue: Double($0.amount)), + start: $0.date, + end: $0.date, + metadata: [ + HKMetadataKeyInsulinDeliveryReason: NSNumber(2), + HKMetadataKeyExternalUUID: $0.id, + HKMetadataKeySyncIdentifier: $0.id, + HKMetadataKeySyncVersion: 1, + Config.freeAPSMetaKey: true + ] + ) + } + + let basalSamples = basal + .map { + HKQuantitySample( + type: sampleType, + quantity: HKQuantity(unit: .internationalUnit(), doubleValue: Double($0.amount)), + start: $0.startDelivery, + end: $0.endDelivery, + metadata: [ + HKMetadataKeyInsulinDeliveryReason: NSNumber(1), + HKMetadataKeyExternalUUID: $0.id, + HKMetadataKeySyncIdentifier: $0.id, + HKMetadataKeySyncVersion: 1, + Config.freeAPSMetaKey: true + ] + ) + } + + self.healthKitStore.save(bolusSamples + basalSamples) { _, _ in } + }) + .store(in: &lifetime) + } + + // MARK: - Observers & Background data delivery + + private func createObserver() { guard settingsManager.settings.useAppleHealth else { return } - guard let bgType = Config.healthBGObject else { + createBGObserver() + createCarbsObserver() + } + + private func createBGObserver() { + guard let type = Config.healthBGObject else { warning(.service, "Can not create HealthKit Observer, because unable to get the Blood Glucose type") return } - - let query = HKObserverQuery(sampleType: bgType, predicate: nil) { [weak self] _, _, observerError in + let query = HKObserverQuery(sampleType: type, predicate: nil) { [weak self] _, _, observerError in guard let self = self else { return } - debug(.service, "Execute HelathKit observer query for loading increment samples") + debug(.service, "Execute HealthKit observer query for loading increment samples") guard observerError == nil else { - warning(.service, "Error during execution of HelathKit Observer's query", error: observerError!) + warning(.service, "Error during execution of HealthKit Observer's query", error: observerError!) return } - if let incrementQuery = self.getBloodGlucoseHKQuery(predicate: self.loadBGPredicate) { - debug(.service, "Create increment query") + if let incrementQuery = self.getBloodGlucoseHKQuery(predicate: self.loadHealthDataPredicate) { + debug(.service, "Create increment query for loading bg") self.healthKitStore.execute(incrementQuery) } } @@ -178,7 +323,29 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { debug(.service, "Create Observer for Blood Glucose") } - func enableBackgroundDelivery() { + private func createCarbsObserver() { + guard let type = Config.healthCarbObject else { + warning(.service, "Can not create HealthKit Observer, because unable to get the Carbs type") + return + } + let query = HKObserverQuery(sampleType: type, predicate: nil) { [weak self] _, _, observerError in + guard let self = self else { return } + debug(.service, "Execute HealthKit observer query for loading increment samples") + guard observerError == nil else { + warning(.service, "Error during execution of HealthKit Observer's query", error: observerError!) + return + } + + if let incrementQuery = self.getCarbsHKQuery(predicate: self.loadHealthDataPredicate) { + debug(.service, "Create increment query for loading carbs") + self.healthKitStore.execute(incrementQuery) + } + } + healthKitStore.execute(query) + debug(.service, "Create Observer for Carbs") + } + + private func enableBackgroundDelivery() { guard settingsManager.settings.useAppleHealth else { healthKitStore.disableAllBackgroundDelivery { _, _ in } return } @@ -193,10 +360,26 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { healthKitStore.enableBackgroundDelivery(for: bgType, frequency: .immediate) { status, error in guard error == nil else { - warning(.service, "Can not enable background delivery", error: error) + warning(.service, "Can not enable background delivery for bg", error: error) return } - debug(.service, "Background delivery status is \(status)") + debug(.service, "Background bg delivery status is \(status)") + } + + guard let carbsType = Config.healthCarbObject else { + warning( + .service, + "Can not create background delivery, because unable to get the Carbs type" + ) + return + } + + healthKitStore.enableBackgroundDelivery(for: carbsType, frequency: .immediate) { status, error in + guard error == nil else { + warning(.service, "Can not enable background delivery for carbs", error: error) + return + } + debug(.service, "Background carbs delivery status is \(status)") } } @@ -234,7 +417,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { ) { [weak self] _, addedObjects, _, anchor, _ in guard let self = self else { return } self.processQueue.async { - debug(.service, "AnchoredQuery did execute") + debug(.service, "BG AnchoredQuery did execute") self.lastBloodGlucoseQueryAnchor = anchor @@ -242,22 +425,80 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { if let bgSamples = addedObjects as? [HKQuantitySample], bgSamples.isNotEmpty { - self.prepareSamplesToPublisherFetch(bgSamples) + self.prepareBGSamplesToPublisherFetch(bgSamples) } } } return query } - private func prepareSamplesToPublisherFetch(_ samples: [HKQuantitySample]) { + private func getCarbsHKQuery(predicate: NSPredicate) -> HKQuery? { + guard let sampleType = Config.healthCarbObject else { return nil } + + let query = HKAnchoredObjectQuery( + type: sampleType, + predicate: predicate, + anchor: lastCarbsQueryAnchor, + limit: HKObjectQueryNoLimit + ) { [weak self] _, addedObjects, _, anchor, _ in + guard let self = self else { return } + self.processQueue.async { + debug(.service, "Carbs AnchoredQuery did execute") + + self.lastCarbsQueryAnchor = anchor + + // Added objects + if let samples = addedObjects as? [HKQuantitySample], + samples.isNotEmpty + { + self.prepareCarbsSamplesToPublisherFetch(samples) + } + } + } + return query + } + + private func prepareCarbsSamplesToPublisherFetch(_ samples: [HKQuantitySample]) { + dispatchPrecondition(condition: .onQueue(processQueue)) + debug(.service, "Start preparing carbs samples: \(String(describing: samples))") + + newCarbs += samples + .compactMap { sample -> HealthKitCarbsSample? in + let fromFAX = sample.metadata?[Config.freeAPSMetaKey] as? Bool ?? false + guard !fromFAX else { return nil } + return HealthKitCarbsSample( + healthKitId: sample.uuid.uuidString, + date: sample.startDate, + carbs: Decimal(round(sample.quantity.doubleValue(for: .gram()))) + ) + } + .map { sample in + CarbsEntry( + id: sample.healthKitId, + createdAt: sample.date, + carbs: sample.carbs, + enteredBy: "applehealth" + ) + } + .filter { $0.createdAt >= Date().addingTimeInterval(-1.days.timeInterval) } + + newCarbs = newCarbs.removeDublicates() + + debug( + .service, + "Current Carbs.Type objects will be send from Publisher during fetch: \(String(describing: newCarbs))" + ) + } + + private func prepareBGSamplesToPublisherFetch(_ samples: [HKQuantitySample]) { dispatchPrecondition(condition: .onQueue(processQueue)) - debug(.service, "Start preparing samples: \(String(describing: samples))") + debug(.service, "Start preparing bg samples: \(String(describing: samples))") newGlucose += samples - .compactMap { sample -> HealthKitSample? in + .compactMap { sample -> HealthKitBGSample? in let fromFAX = sample.metadata?[Config.freeAPSMetaKey] as? Bool ?? false guard !fromFAX else { return nil } - return HealthKitSample( + return HealthKitBGSample( healthKitId: sample.uuid.uuidString, date: sample.startDate, glucose: Int(round(sample.quantity.doubleValue(for: .milligramsPerDeciliter))) @@ -287,6 +528,65 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { ) } + // MARK: - Carbs source + + func fetchCarbs() -> AnyPublisher<[CarbsEntry], Never> { + Future { [weak self] promise in + guard let self = self else { + promise(.success([])) + return + } + + self.processQueue.async { + debug(.service, "Start fetching carbs from HealthKitManager") + guard self.settingsManager.settings.useAppleHealth else { + debug(.service, "HealthKitManager cant return any data, because useAppleHealth option is disable") + promise(.success([])) + return + } + + // Remove old Carbs + self.newCarbs = self.newCarbs + .filter { $0.createdAt >= Date().addingTimeInterval(-1.days.timeInterval) } + // Get actual Carbs (beetwen Date() - 1 day and Date()) + let actualCarbs = self.newCarbs + .filter { $0.createdAt <= Date() } + // Update newCarbs + self.newCarbs = self.newCarbs + .filter { !actualCarbs.contains($0) } + + debug(.service, "Actual carbs is \(actualCarbs)") + + debug(.service, "Current state of newCarbs is \(self.newCarbs)") + + promise(.success(actualCarbs)) + } + } + .eraseToAnyPublisher() + } + + func deleteCarbs(syncID: String) { + guard settingsManager.settings.useAppleHealth, + let sampleType = Config.healthCarbObject, + checkAvailabilitySave(objectTypeToHealthStore: sampleType) + else { return } + + processQueue.async { + let predicate = HKQuery.predicateForObjects( + withMetadataKey: HKMetadataKeySyncIdentifier, + operatorType: .equalTo, + value: syncID + ) + + self.healthKitStore.deleteObjects(of: sampleType, predicate: predicate) { _, _, error in + guard let error = error else { return } + warning(.service, "Cannot delete sample with syncID: \(syncID)", error: error) + } + } + } + + // MARK: - Glucose source + func fetch() -> AnyPublisher<[BloodGlucose], Never> { Future { [weak self] promise in guard let self = self else { @@ -295,7 +595,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { } self.processQueue.async { - debug(.service, "Start fetching HealthKitManager") + debug(.service, "Start fetching bloodGlucose from HealthKitManager") guard self.settingsManager.settings.useAppleHealth else { debug(.service, "HealthKitManager cant return any data, because useAppleHealth option is disable") promise(.success([])) @@ -322,7 +622,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { .eraseToAnyPublisher() } - func deleteGlucise(syncID: String) { + func deleteGlucose(syncID: String) { guard settingsManager.settings.useAppleHealth, let sampleType = Config.healthBGObject, checkAvailabilitySave(objectTypeToHealthStore: sampleType) @@ -343,6 +643,14 @@ final class BaseHealthKitManager: HealthKitManager, Injectable { } } +extension BaseHealthKitManager: CarbsObserver { + func carbsDidUpdate(_ carbs: [CarbsEntry]) { + saveIfNeeded(carbs: carbs) + } +} + +// MARK: Subtypes + enum HealthKitPermissionRequestStatus { case needRequest case didRequest @@ -354,3 +662,16 @@ enum HKError: Error { // Some data can be not available on current iOS-device case dataNotAvailable } + +private struct InsulinBolus { + var id: String + var amount: Decimal + var date: Date +} + +private struct InsulinBasal { + var id: String + var amount: Decimal + var startDelivery: Date + var endDelivery: Date +} diff --git a/FreeAPS/Sources/Services/Migration/MigrationManager.swift b/FreeAPS/Sources/Services/Migration/MigrationManager.swift index fc7595f40..a861d9877 100644 --- a/FreeAPS/Sources/Services/Migration/MigrationManager.swift +++ b/FreeAPS/Sources/Services/Migration/MigrationManager.swift @@ -1,3 +1,7 @@ +/** + Main Migration tool of App + */ + import Combine import Foundation import SwiftUI diff --git a/FreeAPS/Sources/Services/Migration/MigrationPublisher.swift b/FreeAPS/Sources/Services/Migration/MigrationPublisher.swift index cda0e9439..384c27b08 100644 --- a/FreeAPS/Sources/Services/Migration/MigrationPublisher.swift +++ b/FreeAPS/Sources/Services/Migration/MigrationPublisher.swift @@ -1,3 +1,7 @@ +/** + This file store code for working Migration manager with Combine + This is only combine's wrapper for Migration manager + */ import Combine import Foundation import SwiftUI diff --git a/FreeAPS/Sources/Services/Migration/MigrationWorkItem.swift b/FreeAPS/Sources/Services/Migration/MigrationWorkItem.swift index 26f1762b3..b829a09cc 100644 --- a/FreeAPS/Sources/Services/Migration/MigrationWorkItem.swift +++ b/FreeAPS/Sources/Services/Migration/MigrationWorkItem.swift @@ -1,3 +1,11 @@ +/** + This file contains WorkItems with migration tasks + Each WorkItem have to execute one migration task + Each WorkItem can be run in Migration.StateModel.runMigration() + ... + .migrate(startAtVersion: "0.2.6", MigrationWorkExample()) + ... + */ import Foundation protocol MigrationWorkItem { @@ -16,3 +24,26 @@ class MigrationWorkExample: MigrationWorkItem { debug(.businessLogic, "Migration MigrationWorkExample will start") } } + +// MARK: - Migration carbs at 0.2.6 + +// New CarbsEntry class (with new id property) was create. +// This work item add new property to carbs in carbs.json + +class MigrationCarbs: MigrationWorkItem { + private(set) var repeatEachTime: Bool = false + private(set) var uniqueIdentifier: String = "Migration.MigrationCarbs" + func migrationHandler(_: AppInfo) { + let resolver = FreeAPSApp.resolver + let fileStorage = resolver.resolve(FileStorage.self)! + let carbsStorage = resolver.resolve(CarbsStorage.self)! + guard let oldCarbs = fileStorage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntryTill026].self) else { return } + carbsStorage.storeCarbs(convert(carbs: oldCarbs)) + } + + private func convert(carbs: [CarbsEntryTill026]) -> [CarbsEntry] { + carbs.map { oldCarb in + CarbsEntry(createdAt: oldCarb.createdAt, carbs: oldCarb.carbs, enteredBy: oldCarb.enteredBy) + } + } +} diff --git a/FreeAPS/Sources/Services/Network/NightscoutManager.swift b/FreeAPS/Sources/Services/Network/NightscoutManager.swift index c6b20767c..3a4d2ad3f 100644 --- a/FreeAPS/Sources/Services/Network/NightscoutManager.swift +++ b/FreeAPS/Sources/Services/Network/NightscoutManager.swift @@ -3,7 +3,7 @@ import Foundation import Swinject import UIKit -protocol NightscoutManager: GlucoseSource { +protocol NightscoutManager: GlucoseSource, CarbsSource { func fetchGlucose(since date: Date) -> AnyPublisher<[BloodGlucose], Never> func fetchCarbs() -> AnyPublisher<[CarbsEntry], Never> func fetchTempTargets() -> AnyPublisher<[TempTarget], Never>