diff --git a/NightscoutService.xcodeproj/project.pbxproj b/NightscoutService.xcodeproj/project.pbxproj index e3c6429..c97bbcb 100644 --- a/NightscoutService.xcodeproj/project.pbxproj +++ b/NightscoutService.xcodeproj/project.pbxproj @@ -15,7 +15,6 @@ A90E39A122BC773A0016DFE8 /* NightscoutUploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90E39A022BC773A0016DFE8 /* NightscoutUploader.swift */; }; A90E39A322BC782C0016DFE8 /* SyncCarbObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90E39A222BC782C0016DFE8 /* SyncCarbObject.swift */; }; A90E39A522BC791E0016DFE8 /* DoseEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90E39A422BC791E0016DFE8 /* DoseEntry.swift */; }; - A90E39A722BC7A360016DFE8 /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90E39A622BC7A360016DFE8 /* HKUnit.swift */; }; A90E39A922BC7AD10016DFE8 /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90E39A822BC7AD10016DFE8 /* Bundle.swift */; }; A91BAC2522BC691A00ABF1BB /* NightscoutServiceKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A91BAC1B22BC691A00ABF1BB /* NightscoutServiceKit.framework */; }; A91BAC2C22BC691A00ABF1BB /* NightscoutServiceKit.h in Headers */ = {isa = PBXBuildFile; fileRef = A91BAC1E22BC691A00ABF1BB /* NightscoutServiceKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -145,7 +144,6 @@ A90E39A022BC773A0016DFE8 /* NightscoutUploader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutUploader.swift; sourceTree = ""; }; A90E39A222BC782C0016DFE8 /* SyncCarbObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncCarbObject.swift; sourceTree = ""; }; A90E39A422BC791E0016DFE8 /* DoseEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DoseEntry.swift; sourceTree = ""; }; - A90E39A622BC7A360016DFE8 /* HKUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HKUnit.swift; sourceTree = ""; }; A90E39A822BC7AD10016DFE8 /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = ""; }; A91BAC1B22BC691A00ABF1BB /* NightscoutServiceKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = NightscoutServiceKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; A91BAC1E22BC691A00ABF1BB /* NightscoutServiceKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NightscoutServiceKit.h; sourceTree = ""; }; @@ -259,7 +257,6 @@ isa = PBXGroup; children = ( A90E39A422BC791E0016DFE8 /* DoseEntry.swift */, - A90E39A622BC7A360016DFE8 /* HKUnit.swift */, A90E39A022BC773A0016DFE8 /* NightscoutUploader.swift */, C1F7822727CD57A100C0919A /* OverrideTreament.swift */, C177C4512ABE2D8900911B56 /* PersistedCgmEvent.swift */, @@ -729,7 +726,6 @@ A941B08929BCB99C00F91340 /* RemoteCommandValidator.swift in Sources */, C1398D2027C41E3D00416AD6 /* ProfileSet.swift in Sources */, A9AA6E6329EB07D7008FFA78 /* Action.swift in Sources */, - A90E39A722BC7A360016DFE8 /* HKUnit.swift in Sources */, A99A115E29AA28EE007919CE /* RemoteCommandSourceV1.swift in Sources */, A9246B2526231B3A00CCDCB3 /* OTPManager.swift in Sources */, A90E399E22BC76DB0016DFE8 /* TimeInterval.swift in Sources */, @@ -919,7 +915,7 @@ GCC_WARN_UNUSED_LABEL = YES; GCC_WARN_UNUSED_PARAMETER = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.1; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; LOCALIZED_STRING_MACRO_NAMES = ( NSLocalizedString, @@ -1025,7 +1021,7 @@ GCC_WARN_UNUSED_LABEL = YES; GCC_WARN_UNUSED_PARAMETER = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.1; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; LOCALIZED_STRING_MACRO_NAMES = ( NSLocalizedString, diff --git a/NightscoutServiceKit/Extensions/HKUnit.swift b/NightscoutServiceKit/Extensions/HKUnit.swift deleted file mode 100644 index ebed5a3..0000000 --- a/NightscoutServiceKit/Extensions/HKUnit.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// HKUnit.swift -// NightscoutServiceKit -// -// Created by Darin Krauss on 6/20/19. -// Copyright © 2019 LoopKit Authors. All rights reserved. -// - -import HealthKit - -extension HKUnit { - - static let milligramsPerDeciliter: HKUnit = { - return HKUnit.gramUnit(with: .milli).unitDivided(by: .literUnit(with: .deci)) - }() - - static let millimolesPerLiter: HKUnit = { - return HKUnit.moleUnit(with: .milli, molarMass: HKUnitMolarMassBloodGlucose).unitDivided(by: .liter()) - }() - - static let milligramsPerDeciliterPerMinute: HKUnit = { - return HKUnit.milligramsPerDeciliter.unitDivided(by: .minute()) - }() - -} diff --git a/NightscoutServiceKit/Extensions/OverrideTreament.swift b/NightscoutServiceKit/Extensions/OverrideTreament.swift index 0471a0f..ad780f1 100644 --- a/NightscoutServiceKit/Extensions/OverrideTreament.swift +++ b/NightscoutServiceKit/Extensions/OverrideTreament.swift @@ -7,15 +7,15 @@ // import Foundation -import NightscoutKit +import LoopAlgorithm import LoopKit -import HealthKit +import NightscoutKit extension OverrideTreatment { convenience init(override: LoopKit.TemporaryScheduleOverride) { // NS Treatments should be in mg/dL - let unit: HKUnit = .milligramsPerDeciliter + let unit: LoopUnit = .milligramsPerDeciliter let nsTargetRange: ClosedRange? if let targetRange = override.settings.targetRange { @@ -30,12 +30,16 @@ extension OverrideTreatment { switch override.context { case .custom: reason = NSLocalizedString("Custom Override", comment: "Name of custom override") - case .legacyWorkout: - reason = NSLocalizedString("Workout", comment: "Name of legacy workout override") + case .activity(let activity): + reason = activity.activityType.name + NSLocalizedString("Activity", comment: "Suffix added to the name of an activity override") case .preMeal: - reason = NSLocalizedString("Pre-Meal", comment: "Name of pre-meal workout override") + reason = NSLocalizedString("Pre-Meal", comment: "Name of pre-meal override") case .preset(let preset): - reason = preset.symbol + " " + preset.name + if let symbol = preset.symbol, symbol.symbolType == .emoji { + reason = symbol.value + " " + preset.name + } else { + reason = preset.name + } } let remoteAddress: String? diff --git a/NightscoutServiceKit/Extensions/ProfileSet.swift b/NightscoutServiceKit/Extensions/ProfileSet.swift index 3607775..76bca50 100644 --- a/NightscoutServiceKit/Extensions/ProfileSet.swift +++ b/NightscoutServiceKit/Extensions/ProfileSet.swift @@ -7,22 +7,22 @@ // import Foundation -import NightscoutKit +import LoopAlgorithm import LoopKit -import HealthKit +import NightscoutKit -private extension HKUnit { - static func glucoseUnitFromNightscoutUnitString(_ unitString: String) -> HKUnit? { +private extension LoopUnit { + static func glucoseUnitFromNightscoutUnitString(_ unitString: String) -> LoopUnit? { // Some versions of Loop incorrectly uploaded units with // special characters to avoid line breaking. - if unitString == HKUnit.millimolesPerLiter.shortLocalizedUnitString() || - unitString == HKUnit.millimolesPerLiter.shortLocalizedUnitString(avoidLineBreaking: false) + if unitString == LoopUnit.millimolesPerLiter.shortLocalizedUnitString() || + unitString == LoopUnit.millimolesPerLiter.shortLocalizedUnitString(avoidLineBreaking: false) { return .millimolesPerLiter } - if unitString == HKUnit.milligramsPerDeciliter.shortLocalizedUnitString() || - unitString == HKUnit.milligramsPerDeciliter.shortLocalizedUnitString(avoidLineBreaking: false) + if unitString == LoopUnit.milligramsPerDeciliter.shortLocalizedUnitString() || + unitString == LoopUnit.milligramsPerDeciliter.shortLocalizedUnitString(avoidLineBreaking: false) { return .milligramsPerDeciliter } @@ -36,14 +36,14 @@ extension ProfileSet { guard let profile = store["Default"], let glucoseSafetyLimit = settings.minimumBGGuard, - let settingsGlucoseUnit = HKUnit.glucoseUnitFromNightscoutUnitString(units) + let settingsGlucoseUnit = LoopUnit.glucoseUnitFromNightscoutUnitString(units) else { return nil } // If units are specified on the schedule, prefer those over the units specified on the ProfileSet - let scheduleGlucoseUnit: HKUnit - if let profileUnitString = profile.units, let profileUnit = HKUnit.glucoseUnitFromNightscoutUnitString(profileUnitString) + let scheduleGlucoseUnit: LoopUnit + if let profileUnitString = profile.units, let profileUnit = LoopUnit.glucoseUnitFromNightscoutUnitString(profileUnitString) { scheduleGlucoseUnit = profileUnit } else { @@ -59,8 +59,7 @@ extension ProfileSet { let correctionRangeOverrides: CorrectionRangeOverrides? if let range = settings.preMealTargetRange { correctionRangeOverrides = CorrectionRangeOverrides( - preMeal: GlucoseRange(minValue: range.lowerBound, maxValue: range.upperBound, unit: settingsGlucoseUnit), - workout: nil // No longer used + preMeal: GlucoseRange(minValue: range.lowerBound, maxValue: range.upperBound, unit: settingsGlucoseUnit) ) } else { correctionRangeOverrides = nil @@ -76,7 +75,7 @@ extension ProfileSet { timeZone: profile.timeZone) let carbSchedule = CarbRatioSchedule( - unit: .gram(), + unit: .gram, dailyItems: profile.carbratio.map { RepeatingScheduleValue(startTime: $0.offset, value: $0.value) }, timeZone: profile.timeZone) @@ -98,7 +97,7 @@ extension ProfileSet { extension NightscoutKit.TemporaryScheduleOverride { - func loopOverride(for unit: HKUnit) -> LoopKit.TemporaryScheduleOverridePreset? { + func loopOverride(for unit: LoopUnit) -> LoopKit.TemporaryPreset? { guard let name = name, let symbol = symbol else { @@ -114,7 +113,7 @@ extension NightscoutKit.TemporaryScheduleOverride { target = nil } - let temporaryOverrideSettings = TemporaryScheduleOverrideSettings( + let temporaryOverrideSettings = TemporaryPresetSettings( unit: unit, targetRange: target, insulinNeedsScaleFactor: insulinNeedsScaleFactor) @@ -127,8 +126,8 @@ extension NightscoutKit.TemporaryScheduleOverride { loopDuration = .finite(duration) } - return TemporaryScheduleOverridePreset( - symbol: symbol, + return TemporaryPreset( + symbol: .emoji(symbol), name: name, settings: temporaryOverrideSettings, duration: loopDuration) diff --git a/NightscoutServiceKit/Extensions/StoredDosingDecision.swift b/NightscoutServiceKit/Extensions/StoredDosingDecision.swift index aba4104..50c53c5 100644 --- a/NightscoutServiceKit/Extensions/StoredDosingDecision.swift +++ b/NightscoutServiceKit/Extensions/StoredDosingDecision.swift @@ -7,7 +7,7 @@ // import Foundation -import HealthKit +import LoopAlgorithm import LoopKit import NightscoutKit @@ -24,14 +24,14 @@ extension StoredDosingDecision { guard let carbsOnBoard = carbsOnBoard else { return nil } - return COBStatus(cob: carbsOnBoard.quantity.doubleValue(for: HKUnit.gram()), timestamp: carbsOnBoard.startDate) + return COBStatus(cob: carbsOnBoard.quantity.doubleValue(for: LoopUnit.gram), timestamp: carbsOnBoard.startDate) } var loopStatusPredicted: PredictedBG? { guard let predictedGlucose = predictedGlucose, let startDate = predictedGlucose.first?.startDate else { return nil } - return PredictedBG(startDate: startDate, values: predictedGlucose.map { $0.quantity }) + return PredictedBG(startDate: startDate, values: predictedGlucose.map { $0.quantity.hkQuantity }) } var loopStatusAutomaticDoseRecommendation: NightscoutKit.AutomaticDoseRecommendation? { @@ -39,13 +39,10 @@ extension StoredDosingDecision { return nil } - let nightscoutTempBasalAdjustment: TempBasalAdjustment? - - if let basalAdjustment = automaticDoseRecommendation.basalAdjustment { - nightscoutTempBasalAdjustment = TempBasalAdjustment(rate: basalAdjustment.unitsPerHour, duration: basalAdjustment.duration) - } else { - nightscoutTempBasalAdjustment = nil - } + let nightscoutTempBasalAdjustment = TempBasalAdjustment( + rate: automaticDoseRecommendation.basalAdjustment.unitsPerHour, + duration: automaticDoseRecommendation.basalAdjustment.duration + ) return NightscoutKit.AutomaticDoseRecommendation( timestamp: date, @@ -61,12 +58,17 @@ extension StoredDosingDecision { } var loopStatusEnacted: LoopEnacted? { - guard let automaticDoseRecommendation = automaticDoseRecommendation, errors.isEmpty else { + guard errors.isEmpty else { return nil } - let tempBasal = automaticDoseRecommendation.basalAdjustment // NS needs to be updated to support an "enacted" field with no rate. Once that happens, we should not report a fake cancel here, and rate/duration should be nil instead of 0 - return LoopEnacted(rate: tempBasal?.unitsPerHour ?? 0, duration: tempBasal?.duration ?? 0, timestamp: date, received: true, bolusVolume: automaticDoseRecommendation.bolusUnits ?? 0) + return LoopEnacted( + rate: enactedTempBasal?.unitsPerHour ?? 0, + duration: enactedTempBasal?.duration ?? 0, + timestamp: date, + received: true, + bolusVolume: enactedBolusAmount ?? 0 + ) } var loopStatusFailureReason: String? { @@ -122,10 +124,10 @@ extension StoredDosingDecision { return NightscoutKit.OverrideStatus(timestamp: date, active: false) } - let unit = glucoseTargetRangeSchedule?.unit ?? HKUnit.milligramsPerDeciliter - let lowerTarget = HKQuantity(unit: unit, doubleValue: glucoseTargetRange.minValue) - let upperTarget = HKQuantity(unit: unit, doubleValue: glucoseTargetRange.maxValue) - let currentCorrectionRange = CorrectionRange(minValue: lowerTarget, maxValue: upperTarget) + let unit = glucoseTargetRangeSchedule?.unit ?? LoopUnit.milligramsPerDeciliter + let lowerTarget = LoopQuantity(unit: unit, doubleValue: glucoseTargetRange.minValue) + let upperTarget = LoopQuantity(unit: unit, doubleValue: glucoseTargetRange.maxValue) + let currentCorrectionRange = CorrectionRange(minValue: lowerTarget.hkQuantity, maxValue: upperTarget.hkQuantity) let duration = scheduleOverride.duration != .indefinite ? round(scheduleOverride.actualEndDate.timeIntervalSince(date)): nil return NightscoutKit.OverrideStatus(name: scheduleOverride.context.name, diff --git a/NightscoutServiceKit/Extensions/StoredSettings.swift b/NightscoutServiceKit/Extensions/StoredSettings.swift index 0bed9c6..2524534 100644 --- a/NightscoutServiceKit/Extensions/StoredSettings.swift +++ b/NightscoutServiceKit/Extensions/StoredSettings.swift @@ -6,7 +6,6 @@ // Copyright © 2019 LoopKit Authors. All rights reserved. // -import HealthKit import LoopKit import NightscoutKit @@ -49,8 +48,8 @@ extension StoredSettings { return NightscoutKit.LoopSettings( dosingEnabled: dosingEnabled, - overridePresets: overridePresets?.map { $0.nsScheduleOverride(for: bloodGlucoseUnit) } ?? [], - scheduleOverride: scheduleOverride?.nsScheduleOverride(for: bloodGlucoseUnit), + overridePresets: overridePresets.map { $0.nsScheduleOverride(for: bloodGlucoseUnit) }, + scheduleOverride: nil, minimumBGGuard: suspendThreshold?.quantity.doubleValue(for: bloodGlucoseUnit), preMealTargetRange: nightscoutPreMealTargetRange, maximumBasalRatePerHour: maximumBasalRatePerHour, diff --git a/NightscoutServiceKit/Extensions/SyncCarbObject.swift b/NightscoutServiceKit/Extensions/SyncCarbObject.swift index 3122df4..0cf1261 100644 --- a/NightscoutServiceKit/Extensions/SyncCarbObject.swift +++ b/NightscoutServiceKit/Extensions/SyncCarbObject.swift @@ -9,7 +9,6 @@ import Foundation import LoopKit import NightscoutKit -import HealthKit extension SyncCarbObject { diff --git a/NightscoutServiceKit/Extensions/TemporaryScheduleOverride.swift b/NightscoutServiceKit/Extensions/TemporaryScheduleOverride.swift index 29bc68f..4b6a5a7 100644 --- a/NightscoutServiceKit/Extensions/TemporaryScheduleOverride.swift +++ b/NightscoutServiceKit/Extensions/TemporaryScheduleOverride.swift @@ -6,13 +6,13 @@ // Copyright © 2019 LoopKit Authors. All rights reserved. // -import HealthKit +import LoopAlgorithm import LoopKit import NightscoutKit extension LoopKit.TemporaryScheduleOverride { - func nsScheduleOverride(for unit: HKUnit) -> NightscoutKit.TemporaryScheduleOverride { + func nsScheduleOverride(for unit: LoopUnit) -> NightscoutKit.TemporaryScheduleOverride { let nsTargetRange: ClosedRange? if let targetRange = settings.targetRange { nsTargetRange = ClosedRange(uncheckedBounds: ( @@ -34,7 +34,7 @@ extension LoopKit.TemporaryScheduleOverride { duration: nsDuration, targetRange: nsTargetRange, insulinNeedsScaleFactor: settings.insulinNeedsScaleFactor, - symbol: context.symbol, + symbol: context.symbol?.textualRepresentation, name: context.name) } @@ -46,8 +46,8 @@ extension LoopKit.TemporaryScheduleOverride.Context { switch self { case .custom: return nil - case .legacyWorkout: - return LocalizedString("Workout", comment: "Name uploaded to Nightscout for legacy workout override") + case .activity(let activity): + return activity.preset.name case .preMeal: return LocalizedString("Pre-Meal", comment: "Name uploaded to Nightscout for Pre-Meal override") case .preset(let preset): @@ -55,10 +55,12 @@ extension LoopKit.TemporaryScheduleOverride.Context { } } - var symbol: String? { + var symbol: PresetSymbol? { switch self { case .preset(let preset): return preset.symbol + case .activity(let activity): + return activity.preset.symbol default: return nil } @@ -66,9 +68,9 @@ extension LoopKit.TemporaryScheduleOverride.Context { } -extension LoopKit.TemporaryScheduleOverridePreset { +extension LoopKit.TemporaryPreset { - func nsScheduleOverride(for unit: HKUnit) -> NightscoutKit.TemporaryScheduleOverride { + func nsScheduleOverride(for unit: LoopUnit) -> NightscoutKit.TemporaryScheduleOverride { let nsTargetRange: ClosedRange? if let targetRange = settings.targetRange { nsTargetRange = ClosedRange(uncheckedBounds: ( @@ -90,8 +92,9 @@ extension LoopKit.TemporaryScheduleOverridePreset { duration: nsDuration, targetRange: nsTargetRange, insulinNeedsScaleFactor: settings.insulinNeedsScaleFactor, - symbol: self.symbol, - name: self.name) + symbol: self.symbol?.textualRepresentation, + name: self.name + ) } } diff --git a/NightscoutServiceKit/Localizable.xcstrings b/NightscoutServiceKit/Localizable.xcstrings index ec6a05b..8132f96 100644 --- a/NightscoutServiceKit/Localizable.xcstrings +++ b/NightscoutServiceKit/Localizable.xcstrings @@ -1,6 +1,9 @@ { "sourceLanguage" : "en", "strings" : { + "Activity" : { + "comment" : "Suffix added to the name of an activity override" + }, "Bolus Entry" : { "comment" : "The remote action name for Bolus Entry", "localizations" : { @@ -887,7 +890,7 @@ } }, "Pre-Meal" : { - "comment" : "Name of pre-meal workout override\nName uploaded to Nightscout for Pre-Meal override", + "comment" : "Name of pre-meal override\nName uploaded to Nightscout for Pre-Meal override", "localizations" : { "ar" : { "stringUnit" : { @@ -1125,6 +1128,7 @@ }, "Workout" : { "comment" : "Name of legacy workout override\nName uploaded to Nightscout for legacy workout override", + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { diff --git a/NightscoutServiceKit/NightscoutService.swift b/NightscoutServiceKit/NightscoutService.swift index 4626630..3fccce4 100644 --- a/NightscoutServiceKit/NightscoutService.swift +++ b/NightscoutServiceKit/NightscoutService.swift @@ -7,7 +7,6 @@ // import os.log -import HealthKit import LoopKit import NightscoutKit @@ -20,7 +19,7 @@ public enum NightscoutServiceError: Error { public final class NightscoutService: Service { - public static let pluginIdentifier = "NightscoutService" + public let pluginIdentifier = "NightscoutService" public static let localizedTitle = LocalizedString("Nightscout", comment: "The title of the Nightscout service") @@ -30,6 +29,8 @@ public final class NightscoutService: Service { public weak var stateDelegate: StatefulPluggableDelegate? + public weak var remoteDataServiceDelegate: RemoteDataServiceDelegate? + public var siteURL: URL? public var apiSecret: String? @@ -153,10 +154,8 @@ public final class NightscoutService: Service { } extension NightscoutService: RemoteDataService { - - public func uploadTemporaryOverrideData(updated: [LoopKit.TemporaryScheduleOverride], deleted: [LoopKit.TemporaryScheduleOverride], completion: @escaping (Result) -> Void) { + public func uploadTemporaryOverrideData(updated: [LoopKit.TemporaryScheduleOverride], deleted: [LoopKit.TemporaryScheduleOverride]) async throws { guard let uploader = uploader else { - completion(.success(true)) return } @@ -164,118 +163,120 @@ extension NightscoutService: RemoteDataService { let deletions = deleted.map { $0.syncIdentifier.uuidString } - uploader.deleteTreatmentsById(deletions, completionHandler: { (error) in - if let error = error { - self.log.error("Overrides deletions failed to delete %{public}@: %{public}@", String(describing: deletions), String(describing: error)) - } else { - if deletions.count > 0 { - self.log.debug("Deleted ids: %@", deletions) - } - uploader.upload(updates) { (result) in - switch result { - case .failure(let error): - self.log.error("Failed to upload overrides %{public}@: %{public}@", String(describing: updates.map {$0.dictionaryRepresentation}), String(describing: error)) - completion(.failure(error)) - case .success: - self.log.debug("Uploaded overrides %@", String(describing: updates.map {$0.dictionaryRepresentation})) - completion(.success(true)) + try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation) -> Void in + uploader.deleteTreatmentsById(deletions, completionHandler: { (error) in + if let error = error { + self.log.error("Overrides deletions failed to delete %{public}@: %{public}@", String(describing: deletions), String(describing: error)) + } else { + if deletions.count > 0 { + self.log.debug("Deleted ids: %@", deletions) + } + uploader.upload(updates) { (result) in + switch result { + case .failure(let error): + self.log.error("Failed to upload overrides %{public}@: %{public}@", String(describing: updates.map {$0.dictionaryRepresentation}), String(describing: error)) + continuation.resume(throwing: error) + case .success: + self.log.debug("Uploaded overrides %@", String(describing: updates.map {$0.dictionaryRepresentation})) + continuation.resume() + } } } - } + }) }) } public var alertDataLimit: Int? { return 1000 } - public func uploadAlertData(_ stored: [SyncAlertObject], completion: @escaping (Result) -> Void) { - completion(.success(false)) + public func uploadAlertData(_ stored: [SyncAlertObject]) async throws { } public var carbDataLimit: Int? { return 1000 } - public func uploadCarbData(created: [SyncCarbObject], updated: [SyncCarbObject], deleted: [SyncCarbObject], completion: @escaping (Result) -> Void) { + public func uploadCarbData(created: [SyncCarbObject], updated: [SyncCarbObject], deleted: [SyncCarbObject]) async throws { guard hasConfiguration, let uploader = uploader else { - completion(.success(true)) return } - uploader.createCarbData(created) { result in - switch result { - case .failure(let error): - completion(.failure(error)) - case .success(let createdObjectIds): - let createdUploaded = !created.isEmpty - let syncIdentifiers = created.map { $0.syncIdentifier } - for (syncIdentifier, objectId) in zip(syncIdentifiers, createdObjectIds) { - if let syncIdentifier = syncIdentifier { - self.objectIdCache.add(syncIdentifier: syncIdentifier, objectId: objectId) + try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation) -> Void in + uploader.createCarbData(created) { result in + switch result { + case .failure(let error): + continuation.resume(throwing: error) + case .success(let createdObjectIds): + let createdUploaded = !created.isEmpty + let syncIdentifiers = created.map { $0.syncIdentifier } + for (syncIdentifier, objectId) in zip(syncIdentifiers, createdObjectIds) { + if let syncIdentifier = syncIdentifier { + self.objectIdCache.add(syncIdentifier: syncIdentifier, objectId: objectId) + } } - } - self.stateDelegate?.pluginDidUpdateState(self) - - uploader.updateCarbData(updated, usingObjectIdCache: self.objectIdCache) { result in - switch result { - case .failure(let error): - completion(.failure(error)) - case .success(let updatedUploaded): - uploader.deleteCarbData(deleted, usingObjectIdCache: self.objectIdCache) { result in - switch result { - case .failure(let error): - completion(.failure(error)) - case .success(let deletedUploaded): - self.objectIdCache.purge(before: Date().addingTimeInterval(-self.objectIdCacheKeepTime)) - self.stateDelegate?.pluginDidUpdateState(self) - completion(.success(createdUploaded || updatedUploaded || deletedUploaded)) + self.stateDelegate?.pluginDidUpdateState(self) + + uploader.updateCarbData(updated, usingObjectIdCache: self.objectIdCache) { result in + switch result { + case .failure(let error): + continuation.resume(throwing: error) + case .success(let updatedUploaded): + uploader.deleteCarbData(deleted, usingObjectIdCache: self.objectIdCache) { result in + switch result { + case .failure(let error): + continuation.resume(throwing: error) + case .success(let deletedUploaded): + self.objectIdCache.purge(before: Date().addingTimeInterval(-self.objectIdCacheKeepTime)) + self.stateDelegate?.pluginDidUpdateState(self) + continuation.resume() + } } } } } } - } + }) } public var doseDataLimit: Int? { return 1000 } - public func uploadDoseData(created: [DoseEntry], deleted: [DoseEntry], completion: @escaping (_ result: Result) -> Void) { + public func uploadDoseData(created: [DoseEntry], deleted: [DoseEntry]) async throws { guard hasConfiguration, let uploader = uploader else { - completion(.success(true)) return } - uploader.createDoses(created, usingObjectIdCache: self.objectIdCache) { (result) in - switch (result) { - case .failure(let error): - completion(.failure(error)) - case .success(let createdObjectIds): - let createdUploaded = !created.isEmpty - let syncIdentifiers = created.map { $0.syncIdentifier } - for (syncIdentifier, objectId) in zip(syncIdentifiers, createdObjectIds) { - if let syncIdentifier = syncIdentifier { - self.objectIdCache.add(syncIdentifier: syncIdentifier, objectId: objectId) + try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation) -> Void in + uploader.createDoses(created, usingObjectIdCache: self.objectIdCache) { (result) in + switch (result) { + case .failure(let error): + continuation.resume(throwing: error) + case .success(let createdObjectIds): + let createdUploaded = !created.isEmpty + let syncIdentifiers = created.map { $0.syncIdentifier } + for (syncIdentifier, objectId) in zip(syncIdentifiers, createdObjectIds) { + if let syncIdentifier = syncIdentifier { + self.objectIdCache.add(syncIdentifier: syncIdentifier, objectId: objectId) + } } - } - self.stateDelegate?.pluginDidUpdateState(self) - - uploader.deleteDoses(deleted.filter { !$0.isMutable }, usingObjectIdCache: self.objectIdCache) { result in - switch result { - case .failure(let error): - completion(.failure(error)) - case .success(let deletedUploaded): - self.objectIdCache.purge(before: Date().addingTimeInterval(-self.objectIdCacheKeepTime)) - self.stateDelegate?.pluginDidUpdateState(self) - completion(.success(createdUploaded || deletedUploaded)) + self.stateDelegate?.pluginDidUpdateState(self) + + uploader.deleteDoses(deleted.filter { !$0.isMutable }, usingObjectIdCache: self.objectIdCache) { result in + switch result { + case .failure(let error): + continuation.resume(throwing: error) + case .success(let deletedUploaded): + self.objectIdCache.purge(before: Date().addingTimeInterval(-self.objectIdCacheKeepTime)) + self.stateDelegate?.pluginDidUpdateState(self) + continuation.resume() + } } } } - } + }) } public var dosingDecisionDataLimit: Int? { return 50 } // Each can be up to 20K bytes of serialized JSON, target ~1M or less - public func uploadDosingDecisionData(_ stored: [StoredDosingDecision], completion: @escaping (Result) -> Void) { + public func uploadDosingDecisionData(_ stored: [StoredDosingDecision]) async throws { guard hasConfiguration, let uploader = uploader else { - completion(.success(true)) return } @@ -297,42 +298,50 @@ extension NightscoutService: RemoteDataService { } guard statuses.count > 0 else { - completion(.success(false)) return } - uploader.uploadDeviceStatuses(statuses) { result in - switch result { - case .success: - self.lastDosingDecisionForAutomaticDose = nil - default: - break + try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation) -> Void in + uploader.uploadDeviceStatuses(statuses) { result in + switch result { + case .success: + self.lastDosingDecisionForAutomaticDose = nil + default: + break + } + continuation.resume() } - completion(result) - } + }) } public var glucoseDataLimit: Int? { return 1000 } - public func uploadGlucoseData(_ stored: [StoredGlucoseSample], completion: @escaping (Result) -> Void) { + public func uploadGlucoseData(_ stored: [StoredGlucoseSample]) async throws { guard hasConfiguration, let uploader = uploader else { - completion(.success(true)) return } - uploader.uploadGlucoseSamples(stored, completion: completion) + try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation) -> Void in + uploader.uploadGlucoseSamples(stored) { result in + switch result { + case .success: + continuation.resume() + case .failure(let error): + continuation.resume(throwing: error) + } + } + }) } public var pumpEventDataLimit: Int? { return 1000 } - public func uploadPumpEventData(_ stored: [PersistedPumpEvent], completion: @escaping (Result) -> Void) { + public func uploadPumpEventData(_ stored: [PersistedPumpEvent]) async throws { guard hasConfiguration, let uploader = uploader else { - completion(.success(true)) return } - let source = "loop://\(UIDevice.current.name)" + let source = "loop://\(await UIDevice.current.name)" let treatments = stored.compactMap { (event) -> NightscoutTreatment? in // ignore doses; we'll get those via uploadDoseData @@ -342,29 +351,37 @@ extension NightscoutService: RemoteDataService { return event.treatment(source: source) } - uploader.upload(treatments) { (result) in - switch result { - case .failure(let error): - self.log.error("Failed to upload pump events %{public}@: %{public}@", String(describing: treatments.map {$0.dictionaryRepresentation}), String(describing: error)) - completion(.failure(error)) - case .success: - self.log.debug("Uploaded overrides %@", String(describing: treatments.map {$0.dictionaryRepresentation})) - completion(.success(true)) + try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation) -> Void in + uploader.upload(treatments) { (result) in + switch result { + case .failure(let error): + self.log.error("Failed to upload pump events %{public}@: %{public}@", String(describing: treatments.map {$0.dictionaryRepresentation}), String(describing: error)) + continuation.resume(throwing: error) + case .success: + self.log.debug("Uploaded overrides %@", String(describing: treatments.map {$0.dictionaryRepresentation})) + continuation.resume() + } } - } - - completion(.success(false)) + }) } public var settingsDataLimit: Int? { return 400 } // Each can be up to 2.5K bytes of serialized JSON, target ~1M or less - public func uploadSettingsData(_ stored: [StoredSettings], completion: @escaping (Result) -> Void) { + public func uploadSettingsData(_ stored: [StoredSettings]) async throws { guard hasConfiguration, let uploader = uploader else { - completion(.success(true)) return } - uploader.uploadProfiles(stored.compactMap { $0.profileSet }, completion: completion) + try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation) -> Void in + uploader.uploadProfiles(stored.compactMap { $0.profileSet }) { result in + switch result { + case .success: + continuation.resume() + case .failure(let error): + continuation.resume(throwing: error) + } + } + }) } public func fetchStoredTherapySettings(completion: @escaping (Result<(TherapySettings,Date), Error>) -> Void) { @@ -388,13 +405,21 @@ extension NightscoutService: RemoteDataService { }) } - public func uploadCgmEventData(_ stored: [LoopKit.PersistedCgmEvent], completion: @escaping (Result) -> Void) { + public func uploadCgmEventData(_ stored: [LoopKit.PersistedCgmEvent]) async throws { guard hasConfiguration, let uploader = uploader else { - completion(.success(true)) return } - uploader.uploadCgmEvents(stored, completion: completion) + try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation) -> Void in + uploader.uploadCgmEvents(stored) { result in + switch result { + case .success: + continuation.resume() + case .failure(let error): + continuation.resume(throwing: error) + } + } + }) } @@ -444,7 +469,7 @@ extension NightscoutService: RemoteCommandSourceV1Delegate { case .bolusEntry(let bolusCommand): commandType = .bolus - try await self.serviceDelegate?.deliverRemoteBolus(amountInUnits: bolusCommand.amountInUnits) + try await self.serviceDelegate?.deliverRemoteBolus(amountInUnits: bolusCommand.amountInUnits, decisionId: nil) success = true message = String(format: "Bolus of %.2f units delivered successfully", bolusCommand.amountInUnits) diff --git a/NightscoutServiceKitUI/NightscoutService+UI.swift b/NightscoutServiceKitUI/NightscoutService+UI.swift index 94a6921..26d3894 100644 --- a/NightscoutServiceKitUI/NightscoutService+UI.swift +++ b/NightscoutServiceKitUI/NightscoutService+UI.swift @@ -16,11 +16,11 @@ extension NightscoutService: ServiceUI { UIImage(named: "nightscout", in: Bundle(for: ServiceUICoordinator.self), compatibleWith: nil)! } - public static func setupViewController(colorPalette: LoopUIColorPalette, pluginHost: PluginHost) -> SetupUIResult { + public static func setupViewController(colorPalette: LoopUIColorPalette, pluginHost: PluginHost, allowDebugFeatures: Bool) -> SetupUIResult { return .userInteractionRequired(ServiceUICoordinator(colorPalette: colorPalette)) } - public func settingsViewController(colorPalette: LoopUIColorPalette) -> ServiceViewController { + public func settingsViewController(colorPalette: LoopUIColorPalette, allowDebugFeatures: Bool) -> ServiceViewController { return ServiceUICoordinator(service: self, colorPalette: colorPalette) } } diff --git a/NightscoutServiceKitUI/ViewControllers/ServiceUICoordinator.swift b/NightscoutServiceKitUI/ViewControllers/ServiceUICoordinator.swift index 291ff4e..d70797f 100644 --- a/NightscoutServiceKitUI/ViewControllers/ServiceUICoordinator.swift +++ b/NightscoutServiceKitUI/ViewControllers/ServiceUICoordinator.swift @@ -7,7 +7,6 @@ // import Foundation -import HealthKit import SwiftUI import LoopKit import LoopKitUI