From 10e58b7dd1a64d466618f9786841622f443540e2 Mon Sep 17 00:00:00 2001 From: Bill Gestrich <3207996+gestrich@users.noreply.github.com> Date: Sat, 22 Apr 2023 06:31:23 -0400 Subject: [PATCH 01/15] Remote handling rearchitecture --- Loop.xcodeproj/project.pbxproj | 32 -- Loop/Managers/DeviceDataManager.swift | 261 ++++++++---- Loop/Managers/LoopDataManager.swift | 10 + Loop/Managers/NotificationManager.swift | 25 +- Loop/Managers/RemoteDataServicesManager.swift | 59 ++- Loop/Managers/ServicesManager.swift | 42 +- Loop/Models/Remote/BolusAction.swift | 46 -- Loop/Models/Remote/CarbAction.swift | 73 ---- Loop/Models/Remote/OverrideAction.swift | 57 --- .../CarbEntryViewController.swift | 4 +- .../Managers/LoopDataManagerDosingTests.swift | 2 + .../Models/Remote/BolusActionTests.swift | 101 ----- LoopTests/Models/Remote/CarbActionTests.swift | 402 ------------------ .../Models/Remote/OverrideActionTests.swift | 152 ------- 14 files changed, 284 insertions(+), 982 deletions(-) delete mode 100644 Loop/Models/Remote/BolusAction.swift delete mode 100644 Loop/Models/Remote/CarbAction.swift delete mode 100644 Loop/Models/Remote/OverrideAction.swift delete mode 100644 LoopTests/Models/Remote/BolusActionTests.swift delete mode 100644 LoopTests/Models/Remote/CarbActionTests.swift delete mode 100644 LoopTests/Models/Remote/OverrideActionTests.swift diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index d9e4871882..965fcca42a 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -354,12 +354,6 @@ A98556852493F901000FD662 /* AlertStore+SimulatedCoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98556842493F901000FD662 /* AlertStore+SimulatedCoreData.swift */; }; A987CD4924A58A0100439ADC /* ZipArchive.swift in Sources */ = {isa = PBXBuildFile; fileRef = A987CD4824A58A0100439ADC /* ZipArchive.swift */; }; A999D40624663D18004C89D4 /* PumpManagerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A999D40524663D18004C89D4 /* PumpManagerError.swift */; }; - A99A114229A581F4007919CE /* BolusAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99A114129A581F4007919CE /* BolusAction.swift */; }; - A99A114429A5829A007919CE /* CarbAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99A114329A5829A007919CE /* CarbAction.swift */; }; - A99A114629A582A2007919CE /* OverrideAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99A114529A582A2007919CE /* OverrideAction.swift */; }; - A99A114E29A5879D007919CE /* BolusActionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99A114B29A5879C007919CE /* BolusActionTests.swift */; }; - A99A114F29A5879D007919CE /* CarbActionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99A114C29A5879C007919CE /* CarbActionTests.swift */; }; - A99A115029A5879D007919CE /* OverrideActionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99A114D29A5879C007919CE /* OverrideActionTests.swift */; }; A9A056B324B93C62007CF06D /* CriticalEventLogExportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A056B224B93C62007CF06D /* CriticalEventLogExportView.swift */; }; A9A056B524B94123007CF06D /* CriticalEventLogExportViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A056B424B94123007CF06D /* CriticalEventLogExportViewModel.swift */; }; A9A63F8E246B271600588D5B /* NSTimeInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439897341CD2F7DE00223065 /* NSTimeInterval.swift */; }; @@ -1339,12 +1333,6 @@ A98556842493F901000FD662 /* AlertStore+SimulatedCoreData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlertStore+SimulatedCoreData.swift"; sourceTree = ""; }; A987CD4824A58A0100439ADC /* ZipArchive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZipArchive.swift; sourceTree = ""; }; A999D40524663D18004C89D4 /* PumpManagerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpManagerError.swift; sourceTree = ""; }; - A99A114129A581F4007919CE /* BolusAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusAction.swift; sourceTree = ""; }; - A99A114329A5829A007919CE /* CarbAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbAction.swift; sourceTree = ""; }; - A99A114529A582A2007919CE /* OverrideAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideAction.swift; sourceTree = ""; }; - A99A114B29A5879C007919CE /* BolusActionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BolusActionTests.swift; sourceTree = ""; }; - A99A114C29A5879C007919CE /* CarbActionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CarbActionTests.swift; sourceTree = ""; }; - A99A114D29A5879C007919CE /* OverrideActionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverrideActionTests.swift; sourceTree = ""; }; A9A056B224B93C62007CF06D /* CriticalEventLogExportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CriticalEventLogExportView.swift; sourceTree = ""; }; A9A056B424B94123007CF06D /* CriticalEventLogExportViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CriticalEventLogExportViewModel.swift; sourceTree = ""; }; A9B607AF247F000F00792BE4 /* UserNotifications+Loop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserNotifications+Loop.swift"; sourceTree = ""; }; @@ -2810,19 +2798,6 @@ A99A114029A581D6007919CE /* Remote */ = { isa = PBXGroup; children = ( - A99A114129A581F4007919CE /* BolusAction.swift */, - A99A114329A5829A007919CE /* CarbAction.swift */, - A99A114529A582A2007919CE /* OverrideAction.swift */, - ); - path = Remote; - sourceTree = ""; - }; - A99A114A29A58789007919CE /* Remote */ = { - isa = PBXGroup; - children = ( - A99A114B29A5879C007919CE /* BolusActionTests.swift */, - A99A114C29A5879C007919CE /* CarbActionTests.swift */, - A99A114D29A5879C007919CE /* OverrideActionTests.swift */, ); path = Remote; sourceTree = ""; @@ -2838,7 +2813,6 @@ A9E6DFED246A0460005B1A1C /* Models */ = { isa = PBXGroup; children = ( - A99A114A29A58789007919CE /* Remote */, A9DFAFB224F0415E00950D1E /* CarbBackfillRequestUserInfoTests.swift */, A963B279252CEBAE0062AA12 /* SetBolusUserInfoTests.swift */, A9DFAFB424F048A000950D1E /* WatchHistoricalCarbsTests.swift */, @@ -3868,7 +3842,6 @@ C17824A01E19CF9800D9D25C /* GlucoseThresholdTableViewController.swift in Sources */, 4372E487213C86240068E043 /* SampleValue.swift in Sources */, 437CEEE41CDE5C0A003C8C80 /* UIImage.swift in Sources */, - A99A114229A581F4007919CE /* BolusAction.swift in Sources */, C1201E2C23ECDBD0002DA84A /* WatchContextRequestUserInfo.swift in Sources */, 1D49795824E7289700948F05 /* ServicesViewModel.swift in Sources */, 1D4A3E2D2478628500FD601B /* StoredAlert+CoreDataClass.swift in Sources */, @@ -3929,7 +3902,6 @@ 1D6B1B6726866D89009AC446 /* AlertPermissionsChecker.swift in Sources */, 4F08DE8F1E7BB871006741EA /* CollectionType+Loop.swift in Sources */, A9F703772489D8AA00C98AD8 /* PersistentDeviceLog+SimulatedCoreData.swift in Sources */, - A99A114629A582A2007919CE /* OverrideAction.swift in Sources */, E9B080B1253BDA6300BAD8F8 /* UserDefaults+LoopIntents.swift in Sources */, C1AF062329426300002C1B19 /* ManualGlucoseEntryRow.swift in Sources */, C148CEE724FD91BD00711B3B /* DeliveryUncertaintyAlertManager.swift in Sources */, @@ -3977,7 +3949,6 @@ A9C62D842331700E00535612 /* DiagnosticLog+Subsystem.swift in Sources */, 895FE0952201234000FCF18A /* OverrideSelectionViewController.swift in Sources */, C1EF747228D6A44A00C8C083 /* CrashRecoveryManager.swift in Sources */, - A99A114429A5829A007919CE /* CarbAction.swift in Sources */, A9F66FC3247F451500096EA7 /* UIDevice+Loop.swift in Sources */, 439706E622D2E84900C81566 /* PredictionSettingTableViewCell.swift in Sources */, 430D85891F44037000AF2D4F /* HUDViewTableViewCell.swift in Sources */, @@ -4149,12 +4120,10 @@ buildActionMask = 2147483647; files = ( A9DF02CB24F72B9E00B7C988 /* CriticalEventLogTests.swift in Sources */, - A99A114F29A5879D007919CE /* CarbActionTests.swift in Sources */, B44251B3252350CE00605937 /* ChartAxisValuesStaticGeneratorTests.swift in Sources */, 1D80313D24746274002810DF /* AlertStoreTests.swift in Sources */, C1777A6625A125F100595963 /* ManualEntryDoseViewModelTests.swift in Sources */, C16B984026B4898800256B05 /* DoseEnactorTests.swift in Sources */, - A99A115029A5879D007919CE /* OverrideActionTests.swift in Sources */, A9A63F8E246B271600588D5B /* NSTimeInterval.swift in Sources */, A9DFAFB324F0415E00950D1E /* CarbBackfillRequestUserInfoTests.swift in Sources */, A963B27A252CEBAE0062AA12 /* SetBolusUserInfoTests.swift in Sources */, @@ -4174,7 +4143,6 @@ B4CAD8792549D2540057946B /* LoopCompletionFreshnessTests.swift in Sources */, 1D8D55BC252274650044DBB6 /* BolusEntryViewModelTests.swift in Sources */, A91E4C2124F867A700BE9213 /* StoredAlertTests.swift in Sources */, - A99A114E29A5879D007919CE /* BolusActionTests.swift in Sources */, 1DA7A84224476EAD008257F0 /* AlertManagerTests.swift in Sources */, A91E4C2324F86F1000BE9213 /* CriticalEventLogExportManagerTests.swift in Sources */, E9C58A7324DB4A2700487A17 /* LoopDataManagerTests.swift in Sources */, diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 9c48de6c02..f8c906d972 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -407,7 +407,6 @@ final class DeviceDataManager { overrideHistory: overrideHistory, insulinDeliveryStore: doseStore.insulinDeliveryStore ) - settingsManager.remoteDataServicesManager = remoteDataServicesManager @@ -416,7 +415,8 @@ final class DeviceDataManager { alertManager: alertManager, analyticsServicesManager: analyticsServicesManager, loggingServicesManager: loggingServicesManager, - remoteDataServicesManager: remoteDataServicesManager + remoteDataServicesManager: remoteDataServicesManager, + remoteActionDelegate: self ) let criticalEventLogs: [CriticalEventLog] = [settingsManager.settingsStore, glucoseStore, carbStore, dosingDecisionStore, doseStore, deviceLog, alertManager.alertStore] @@ -1339,6 +1339,15 @@ extension DeviceDataManager: LoopDataManagerDelegate { self.crashRecoveryManager.dosingFinished() } } + + func loopDataManager( + _ manager: LoopDataManager, + loopDidFinishWithDosingDecision: + StoredDosingDecision, error: LoopError? + ) { + processPendingRemoteCommands() + } + } extension Notification.Name { @@ -1348,84 +1357,74 @@ extension Notification.Name { } // MARK: - Remote Notification Handling -extension DeviceDataManager { + +extension DeviceDataManager: RemoteActionDelegate { func handleRemoteNotification(_ notification: [String: AnyObject]) { Task { - let backgroundTask = await beginBackgroundTask(name: "Remote Data Upload") - await handleRemoteNotification(notification) + log.default("Remote Notification: Handling notification %{public}@", notification) + + guard FeatureFlags.remoteCommandsEnabled else { + log.error("Remote Notification: Remote Commands not enabled.") + return + } + + let backgroundTask = await beginBackgroundTask(name: "Handle Remote Notification") + do { + try await remoteDataServicesManager.handleRemoteNotification(notification) + } catch { + log.error("Remote Notification: Error: %{public}@", String(describing: error)) + } + await endBackgroundTask(backgroundTask) + log.default("Remote Notification: Finished handling") } } - func handleRemoteNotification(_ notification: [String: AnyObject]) async { - - defer { - log.default("Remote Notification: Finished handling") - } - - guard FeatureFlags.remoteCommandsEnabled else { - log.error("Remote Notification: Remote Commands not enabled.") - return - } - - let command: RemoteCommand - do { - command = try await remoteDataServicesManager.commandFromPushNotification(notification) - } catch { - log.error("Remote Notification: Parse Error: %{public}@", String(describing: error)) - return + func processPendingRemoteCommands() { + Task { + guard FeatureFlags.remoteCommandsEnabled else { + return + } + + let backgroundTask = await beginBackgroundTask(name: "Handle Pending Remote Commands") + await remoteDataServicesManager.processPendingRemoteCommands() + await endBackgroundTask(backgroundTask) } - - await handleRemoteCommand(command) } - func handleRemoteCommand(_ command: RemoteCommand) async { + //Remote Overrides + + func handleRemoteOverride(name: String, durationTime: TimeInterval?, remoteAddress: String) async throws { - log.default("Remote Notification: Handling command %{public}@", String(describing: command)) + guard let preset = loopManager.settings.overridePresets.first(where: { $0.name == name }) else { + throw OverrideActionError.unknownPreset(name) + } - switch command.action { - case .temporaryScheduleOverride(let overrideAction): - do { - try command.validate() - try await handleOverrideAction(overrideAction) - } catch { - log.error("Remote Notification: Override Action Error: %{public}@", String(describing: error)) - } - case .cancelTemporaryOverride(let overrideCancelAction): - do { - try command.validate() - try await handleOverrideCancelAction(overrideCancelAction) - } catch { - log.error("Remote Notification: Override Action Cancel Error: %{public}@", String(describing: error)) + var remoteOverride = preset.createOverride(enactTrigger: .remote(remoteAddress)) + + if let durationTime = durationTime { + + guard durationTime <= LoopConstants.maxOverrideDurationTime else { + throw OverrideActionError.durationExceedsMax(LoopConstants.maxOverrideDurationTime) } - case .bolusEntry(let bolusAction): - do { - try command.validate() - try await handleBolusAction(bolusAction) - } catch { - await NotificationManager.sendRemoteBolusFailureNotification(for: error, amount: bolusAction.amountInUnits) - log.error("Remote Notification: Bolus Action Error: %{public}@", String(describing: error)) + + guard durationTime >= 0 else { + throw OverrideActionError.negativeDuration } - case .carbsEntry(let carbAction): - do { - try command.validate() - try await handleCarbAction(carbAction) - } catch { - await NotificationManager.sendRemoteCarbEntryFailureNotification(for: error, amountInGrams: carbAction.amountInGrams) - log.error("Remote Notification: Carb Action Error: %{public}@", String(describing: error)) + + if durationTime == 0 { + remoteOverride.duration = .indefinite + } else { + remoteOverride.duration = .finite(durationTime) } } - } - - //Remote Overrides - - func handleOverrideAction(_ action: OverrideAction) async throws { - let remoteOverride = try action.toValidOverride(allowedPresets: loopManager.settings.overridePresets) + await activateRemoteOverride(remoteOverride) } - func handleOverrideCancelAction(_ action: OverrideCancelAction) async throws { + + func handleRemoteOverrideCancel() async throws { await activateRemoteOverride(nil) } @@ -1434,30 +1433,146 @@ extension DeviceDataManager { await remoteDataServicesManager.triggerUpload(for: .overrides) } + enum OverrideActionError: LocalizedError { + + case unknownPreset(String) + case durationExceedsMax(TimeInterval) + case negativeDuration + + var errorDescription: String? { + switch self { + case .unknownPreset(let presetName): + return String(format: NSLocalizedString("Unknown preset: %1$@", comment: "Remote command error description: unknown preset (1: preset name)."), presetName) + case .durationExceedsMax(let maxDurationTime): + return String(format: NSLocalizedString("Duration exceeds: %1$.1f hours", comment: "Remote command error description: duration exceed max (1: max duration in hours)."), maxDurationTime.hours) + case .negativeDuration: + return String(format: NSLocalizedString("Negative duration not allowed", comment: "Remote command error description: negative duration error.")) + } + } + } + //Remote Bolus - func handleBolusAction(_ action: BolusAction) async throws { - let validBolusAmount = try action.toValidBolusAmount(maximumBolus: loopManager.settings.maximumBolus) - try await self.enactBolus(units: validBolusAmount, activationType: .manualNoRecommendation) + func handleRemoteBolus(amountInUnits: Double) async throws { + + guard amountInUnits > 0 else { + throw BolusActionError.invalidBolus + } + + guard let maxBolusAmount = loopManager.settings.maximumBolus else { + throw BolusActionError.missingMaxBolus + } + + guard amountInUnits <= maxBolusAmount else { + throw BolusActionError.exceedsMaxBolus + } + + try await self.enactBolus(units: amountInUnits, activationType: .manualNoRecommendation) await remoteDataServicesManager.triggerUpload(for: .dose) - self.analyticsServicesManager.didBolus(source: "Remote", units: validBolusAmount) + self.analyticsServicesManager.didBolus(source: "Remote", units: amountInUnits) + } + + enum BolusActionError: LocalizedError { + + case invalidBolus + case missingMaxBolus + case exceedsMaxBolus + + var errorDescription: String? { + switch self { + case .invalidBolus: + return NSLocalizedString("Invalid Bolus Amount", comment: "Remote command error description: invalid bolus amount.") + case .missingMaxBolus: + return NSLocalizedString("Missing maximum allowed bolus in settings", comment: "Remote command error description: missing maximum bolus in settings.") + case .exceedsMaxBolus: + return NSLocalizedString("Exceeds maximum allowed bolus in settings", comment: "Remote command error description: bolus exceeds maximum bolus in settings.") + } + } } //Remote Carb Entry - func handleCarbAction(_ action: CarbAction) async throws { - let candidateCarbEntry = try action.toValidCarbEntry(defaultAbsorptionTime: carbStore.defaultAbsorptionTimes.medium, - minAbsorptionTime: LoopConstants.minCarbAbsorptionTime, - maxAbsorptionTime: LoopConstants.maxCarbAbsorptionTime, - maxCarbEntryQuantity: LoopConstants.maxCarbEntryQuantity.doubleValue(for: .gram()), - maxCarbEntryPastTime: LoopConstants.maxCarbEntryPastTime, - maxCarbEntryFutureTime: LoopConstants.maxCarbEntryFutureTime - ) + func handleRemoteCarb(amountInGrams: Double, absorptionTime: TimeInterval?, foodType: String?, startDate: Date?) async throws { + + let absorptionTime = absorptionTime ?? carbStore.defaultAbsorptionTimes.medium + if absorptionTime < LoopConstants.minCarbAbsorptionTime || absorptionTime > LoopConstants.maxCarbAbsorptionTime { + throw CarbActionError.invalidAbsorptionTime(absorptionTime) + } + + guard amountInGrams > 0.0 else { + throw CarbActionError.invalidCarbs + } + + guard amountInGrams <= LoopConstants.maxCarbEntryQuantity.doubleValue(for: .gram()) else { + throw CarbActionError.exceedsMaxCarbs + } + + if let startDate = startDate { + let maxStartDate = Date().addingTimeInterval(LoopConstants.maxCarbEntryFutureTime) + let minStartDate = Date().addingTimeInterval(LoopConstants.maxCarbEntryPastTime) + guard startDate <= maxStartDate && startDate >= minStartDate else { + throw CarbActionError.invalidStartDate(startDate) + } + } + + let quantity = HKQuantity(unit: .gram(), doubleValue: amountInGrams) + let candidateCarbEntry = NewCarbEntry(quantity: quantity, startDate: startDate ?? Date(), foodType: foodType, absorptionTime: absorptionTime) let _ = try await addRemoteCarbEntry(candidateCarbEntry) await remoteDataServicesManager.triggerUpload(for: .carb) } + enum CarbActionError: LocalizedError { + + case invalidAbsorptionTime(TimeInterval) + case invalidStartDate(Date) + case exceedsMaxCarbs + case invalidCarbs + + var errorDescription: String? { + switch self { + case .exceedsMaxCarbs: + return NSLocalizedString("Exceeds maximum allowed carbs", comment: "Remote command error description: carbs exceed maximum amount.") + case .invalidCarbs: + return NSLocalizedString("Invalid carb amount", comment: "Remote command error description: invalid carb amount.") + case .invalidAbsorptionTime(let absorptionTime): + let absorptionHoursFormatted = Self.numberFormatter.string(from: absorptionTime.hours) ?? "" + return String(format: NSLocalizedString("Invalid absorption time: %1$@ hours", comment: "Remote command error description: invalid absorption time. (1: Input duration in hours)."), absorptionHoursFormatted) + case .invalidStartDate(let startDate): + let startDateFormatted = Self.dateFormatter.string(from: startDate) + return String(format: NSLocalizedString("Start time is out of range: %@", comment: "Remote command error description: invalid start time is out of range."), startDateFormatted) + } + } + + static var numberFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + return formatter + }() + + static var dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.timeStyle = .medium + return formatter + }() + } + + //Remote Autobolus Update + + func handleRemoteAutobolus(activate: Bool) async throws { + loopManager.mutateSettings { settings in + settings.automaticDosingStrategy = activate ? .automaticBolus : .tempBasalOnly + } + } + + //Remote Closed Loop Update + + func handleRemoteClosedLoop(activate: Bool) async throws { + loopManager.mutateSettings { settings in + settings.dosingEnabled = activate + } + } + //Can't add this concurrency wrapper method to LoopKit due to the minimum iOS version func addRemoteCarbEntry(_ carbEntry: NewCarbEntry) async throws -> StoredCarbEntry { return try await withCheckedThrowingContinuation { continuation in diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index ffc66ee314..0c5455d008 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -905,6 +905,8 @@ extension LoopDataManager { } updateRemoteRecommendation() + + self.delegate?.loopDataManager(self, loopDidFinishWithDosingDecision: dosingDecision, error: error) } fileprivate enum UpdateReason: String { @@ -2206,6 +2208,14 @@ protocol LoopDataManagerDelegate: AnyObject { /// - units: The recommended bolus in U /// - Returns: a supported bolus volume in U. The volume returned should be the nearest deliverable volume. func roundBolusVolume(units: Double) -> Double + + /// Informs the delegate that a Loop finished + /// + /// - Parameters: + /// - manager: The manager + /// - dosingDecision: The resulting dosing decision + /// - error: An error, if any + func loopDataManager(_ manager: LoopDataManager, loopDidFinishWithDosingDecision: StoredDosingDecision, error: LoopError?) /// The pump manager status, if one exists. var pumpManagerStatus: PumpManagerStatus? { get } diff --git a/Loop/Managers/NotificationManager.swift b/Loop/Managers/NotificationManager.swift index c43e3939bb..996d147047 100644 --- a/Loop/Managers/NotificationManager.swift +++ b/Loop/Managers/NotificationManager.swift @@ -79,27 +79,6 @@ extension NotificationManager { // MARK: - Notifications - @MainActor - static func sendRemoteCommandExpiredNotification(timeExpired: TimeInterval) { - let notification = UNMutableNotificationContent() - - notification.title = NSLocalizedString("Remote Command Expired", comment: "The notification title for the remote command expiration error") - - notification.body = String(format: NSLocalizedString("The remote command expired %.0f minutes ago.", comment: "The notification body for a remote command expiration. (1: Expiration in minutes)"), fabs(timeExpired / 60.0)) - notification.sound = .default - - notification.categoryIdentifier = LoopNotificationCategory.remoteCommandExpired.rawValue - - let request = UNNotificationRequest( - // Only support 1 expiration notification at once - identifier: LoopNotificationCategory.remoteCommandExpired.rawValue, - content: notification, - trigger: nil - ) - - UNUserNotificationCenter.current().add(request) - } - static func sendBolusFailureNotification(for error: PumpManagerError, units: Double, at startDate: Date, activationType: BolusActivationType) { let notification = UNMutableNotificationContent() @@ -160,10 +139,10 @@ extension NotificationManager { } @MainActor - static func sendRemoteBolusFailureNotification(for error: Error, amount: Double) { + static func sendRemoteBolusFailureNotification(for error: Error, amountInUnits: Double) { let notification = UNMutableNotificationContent() let quantityFormatter = QuantityFormatter(for: .internationalUnit()) - guard let amountDescription = quantityFormatter.numberFormatter.string(from: amount) else { + guard let amountDescription = quantityFormatter.numberFormatter.string(from: amountInUnits) else { return } diff --git a/Loop/Managers/RemoteDataServicesManager.swift b/Loop/Managers/RemoteDataServicesManager.swift index e8a8ea0868..8f5d4d8586 100644 --- a/Loop/Managers/RemoteDataServicesManager.swift +++ b/Loop/Managers/RemoteDataServicesManager.swift @@ -596,26 +596,6 @@ extension RemoteDataServicesManager { } extension RemoteDataServicesManager { - - func serviceForPushNotification(_ notification: [String: AnyObject]) -> RemoteDataService? { - - let defaultServiceIdentifier = "NightscoutService" - let serviceIdentifier = notification["serviceIdentifier"] as? String ?? defaultServiceIdentifier - return remoteDataServices.first(where: {$0.serviceIdentifier == serviceIdentifier}) - } - - func commandFromPushNotification(_ notification: [String : AnyObject]) async throws -> RemoteCommand { - - enum RemoteDataServicesManagerCommandError: LocalizedError { - case missingNotificationService - } - - guard let service = serviceForPushNotification(notification) else { - throw RemoteDataServicesManagerCommandError.missingNotificationService - } - - return try await service.commandFromPushNotification(notification) - } public func temporaryScheduleOverrideHistoryDidUpdate() { triggerUpload(for: .overrides) @@ -657,6 +637,45 @@ extension RemoteDataServicesManager { } } +//Remote Commands +extension RemoteDataServicesManager { + + public func handleRemoteNotification(_ notification: [String: AnyObject]) async throws { + let service = try serviceForPushNotification(notification) + return try await service.handleRemoteNotification(notification) + } + + func processPendingRemoteCommands() async { + for service in remoteDataServices { + do { + try await service.processPendingRemoteCommands() + } catch { + self.log.error("Error fetching pending commands: %{public}@", String(describing: error)) + } + } + } + + func serviceForPushNotification(_ notification: [String: AnyObject]) throws -> RemoteDataService { + let defaultServiceIdentifier = "NightscoutService" + let serviceIdentifier = notification["serviceIdentifier"] as? String ?? defaultServiceIdentifier + guard let service = remoteDataServices.first(where: {$0.serviceIdentifier == serviceIdentifier}) else { + throw RemoteDataServicesManagerCommandError.unsupportedServiceIdentifier(serviceIdentifier) + } + return service + } + + enum RemoteDataServicesManagerCommandError: LocalizedError { + case unsupportedServiceIdentifier(String) + + var errorDescription: String? { + switch self { + case .unsupportedServiceIdentifier(let serviceIdentifier): + return String(format: NSLocalizedString("Unsupported Notification Service: %1$@", comment: "Error message when a service can't be found to handle a push notification. (1: Service Identifier)"), serviceIdentifier) + } + } + } +} + protocol RemoteDataServicesManagerDelegate: AnyObject { var shouldSyncToRemoteService: Bool {get} } diff --git a/Loop/Managers/ServicesManager.swift b/Loop/Managers/ServicesManager.swift index 3140029e08..0c9a78b5a4 100644 --- a/Loop/Managers/ServicesManager.swift +++ b/Loop/Managers/ServicesManager.swift @@ -23,6 +23,8 @@ class ServicesManager { let remoteDataServicesManager: RemoteDataServicesManager + weak var remoteActionDelegate: RemoteActionDelegate? + private var services = [Service]() private let servicesLock = UnfairLock() @@ -37,13 +39,15 @@ class ServicesManager { alertManager: AlertManager, analyticsServicesManager: AnalyticsServicesManager, loggingServicesManager: LoggingServicesManager, - remoteDataServicesManager: RemoteDataServicesManager + remoteDataServicesManager: RemoteDataServicesManager, + remoteActionDelegate: RemoteActionDelegate ) { self.pluginManager = pluginManager self.alertManager = alertManager self.analyticsServicesManager = analyticsServicesManager self.loggingServicesManager = loggingServicesManager self.remoteDataServicesManager = remoteDataServicesManager + self.remoteActionDelegate = remoteActionDelegate restoreState() } @@ -201,6 +205,42 @@ extension ServicesManager: ServiceDelegate { log.default("Service with identifier '%{public}@' deleted", service.serviceIdentifier) removeActiveService(service) } + + func handleRemoteOverride(name: String, durationTime: TimeInterval?, remoteAddress: String) async throws { + try await remoteActionDelegate?.handleRemoteOverride(name: name, durationTime: durationTime, remoteAddress: remoteAddress) + } + + func handleRemoteOverrideCancel() async throws { + try await remoteActionDelegate?.handleRemoteOverrideCancel() + } + + func handleRemoteCarb(amountInGrams: Double, absorptionTime: TimeInterval?, foodType: String?, startDate: Date?) async throws { + do { + try await remoteActionDelegate?.handleRemoteCarb(amountInGrams: amountInGrams, absorptionTime: absorptionTime, foodType: foodType, startDate: startDate) + await NotificationManager.sendRemoteCarbEntryNotification(amountInGrams: amountInGrams) + } catch { + await NotificationManager.sendRemoteCarbEntryFailureNotification(for: error, amountInGrams: amountInGrams) + throw error + } + } + + func handleRemoteBolus(amountInUnits: Double) async throws { + do { + try await remoteActionDelegate?.handleRemoteBolus(amountInUnits: amountInUnits) + await NotificationManager.sendRemoteBolusNotification(amount: amountInUnits) + } catch { + await NotificationManager.sendRemoteBolusFailureNotification(for: error, amountInUnits: amountInUnits) + throw error + } + } + + func handleRemoteClosedLoop(activate: Bool) async throws { + try await remoteActionDelegate?.handleRemoteClosedLoop(activate: activate) + } + + func handleRemoteAutobolus(activate: Bool) async throws { + try await remoteActionDelegate?.handleRemoteAutobolus(activate: activate) + } } extension ServicesManager: AlertIssuer { diff --git a/Loop/Models/Remote/BolusAction.swift b/Loop/Models/Remote/BolusAction.swift deleted file mode 100644 index 054b618089..0000000000 --- a/Loop/Models/Remote/BolusAction.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// BolusAction.swift -// Loop -// -// Created by Bill Gestrich on 2/21/23. -// Copyright © 2023 LoopKit Authors. All rights reserved. -// - -import LoopKit - -extension BolusAction { - func toValidBolusAmount(maximumBolus: Double?) throws -> Double { - - guard amountInUnits > 0 else { - throw BolusActionError.invalidBolus - } - - guard let maxBolusAmount = maximumBolus else { - throw BolusActionError.missingMaxBolus - } - - guard amountInUnits <= maxBolusAmount else { - throw BolusActionError.exceedsMaxBolus - } - - return amountInUnits - } -} - -enum BolusActionError: LocalizedError { - - case invalidBolus - case missingMaxBolus - case exceedsMaxBolus - - var errorDescription: String? { - switch self { - case .invalidBolus: - return NSLocalizedString("Invalid Bolus Amount", comment: "Remote command error description: invalid bolus amount.") - case .missingMaxBolus: - return NSLocalizedString("Missing maximum allowed bolus in settings", comment: "Remote command error description: missing maximum bolus in settings.") - case .exceedsMaxBolus: - return NSLocalizedString("Exceeds maximum allowed bolus in settings", comment: "Remote command error description: bolus exceeds maximum bolus in settings.") - } - } -} diff --git a/Loop/Models/Remote/CarbAction.swift b/Loop/Models/Remote/CarbAction.swift deleted file mode 100644 index fd00ed9d6a..0000000000 --- a/Loop/Models/Remote/CarbAction.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// CarbAction.swift -// Loop -// -// Created by Bill Gestrich on 2/21/23. -// Copyright © 2023 LoopKit Authors. All rights reserved. -// - -import LoopKit -import HealthKit - -extension CarbAction { - - func toValidCarbEntry(defaultAbsorptionTime: TimeInterval, - minAbsorptionTime: TimeInterval, - maxAbsorptionTime: TimeInterval, - maxCarbEntryQuantity: Double, - maxCarbEntryPastTime: TimeInterval, - maxCarbEntryFutureTime: TimeInterval, - nowDate: Date = Date()) throws -> NewCarbEntry { - - let absorptionTime = absorptionTime ?? defaultAbsorptionTime - if absorptionTime < minAbsorptionTime || absorptionTime > maxAbsorptionTime { - throw CarbActionError.invalidAbsorptionTime(absorptionTime) - } - - guard amountInGrams > 0.0 else { - throw CarbActionError.invalidCarbs - } - - guard amountInGrams <= maxCarbEntryQuantity else { - throw CarbActionError.exceedsMaxCarbs - } - - if let startDate = startDate { - let maxStartDate = nowDate.addingTimeInterval(maxCarbEntryFutureTime) - let minStartDate = nowDate.addingTimeInterval(maxCarbEntryPastTime) - guard startDate <= maxStartDate && startDate >= minStartDate else { - throw CarbActionError.invalidStartDate(startDate) - } - } - - let quantity = HKQuantity(unit: .gram(), doubleValue: amountInGrams) - return NewCarbEntry(quantity: quantity, startDate: startDate ?? nowDate, foodType: foodType, absorptionTime: absorptionTime) - } -} - -enum CarbActionError: LocalizedError { - - case invalidAbsorptionTime(TimeInterval) - case invalidStartDate(Date) - case exceedsMaxCarbs - case invalidCarbs - - var errorDescription: String? { - switch self { - case .exceedsMaxCarbs: - return NSLocalizedString("Exceeds maximum allowed carbs", comment: "Remote command error description: carbs exceed maximum amount.") - case .invalidCarbs: - return NSLocalizedString("Invalid carb amount", comment: "Remote command error description: invalid carb amount.") - case .invalidAbsorptionTime(let absorptionTime): - return String(format: NSLocalizedString("Invalid absorption time: %d hours", comment: "Remote command error description: invalid absorption time."), absorptionTime.hours) - case .invalidStartDate(let startDate): - return String(format: NSLocalizedString("Start time is out of range: %@", comment: "Remote command error description: invalid start time is out of range."), Self.dateFormatter.string(from: startDate)) - } - } - - static var dateFormatter: DateFormatter = { - let formatter = DateFormatter() - formatter.timeStyle = .medium - return formatter - }() -} diff --git a/Loop/Models/Remote/OverrideAction.swift b/Loop/Models/Remote/OverrideAction.swift deleted file mode 100644 index 28ea26e383..0000000000 --- a/Loop/Models/Remote/OverrideAction.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// OverrideAction.swift -// Loop -// -// Created by Bill Gestrich on 2/21/23. -// Copyright © 2023 LoopKit Authors. All rights reserved. -// - -import LoopKit - -extension OverrideAction { - - func toValidOverride(allowedPresets: [TemporaryScheduleOverridePreset]) throws -> TemporaryScheduleOverride { - guard let preset = allowedPresets.first(where: { $0.name == name }) else { - throw OverrideActionError.unknownPreset(name) - } - - var remoteOverride = preset.createOverride(enactTrigger: .remote(remoteAddress)) - - if let durationTime = durationTime { - - guard durationTime <= LoopConstants.maxOverrideDurationTime else { - throw OverrideActionError.durationExceedsMax(LoopConstants.maxOverrideDurationTime) - } - - guard durationTime >= 0 else { - throw OverrideActionError.negativeDuration - } - - if durationTime == 0 { - remoteOverride.duration = .indefinite - } else { - remoteOverride.duration = .finite(durationTime) - } - } - - return remoteOverride - } -} - -enum OverrideActionError: LocalizedError { - - case unknownPreset(String) - case durationExceedsMax(TimeInterval) - case negativeDuration - - var errorDescription: String? { - switch self { - case .unknownPreset(let presetName): - return String(format: NSLocalizedString("Unknown preset: %1$@", comment: "Remote command error description: unknown preset (1: preset name)."), presetName) - case .durationExceedsMax(let maxDurationTime): - return String(format: NSLocalizedString("Duration exceeds: %1$.1f hours", comment: "Remote command error description: duration exceed max (1: max duration in hours)."), maxDurationTime.hours) - case .negativeDuration: - return String(format: NSLocalizedString("Negative duration not allowed", comment: "Remote command error description: negative duration error.")) - } - } -} diff --git a/Loop/View Controllers/CarbEntryViewController.swift b/Loop/View Controllers/CarbEntryViewController.swift index 7a8c950a34..92b7ef7be4 100644 --- a/Loop/View Controllers/CarbEntryViewController.swift +++ b/Loop/View Controllers/CarbEntryViewController.swift @@ -405,8 +405,8 @@ final class CarbEntryViewController: LoopChartsTableViewController, Identifiable cell.datePicker.preferredDatePickerStyle = .wheels } #endif - cell.datePicker.maximumDate = date.addingTimeInterval(.hours(1)) - cell.datePicker.minimumDate = date.addingTimeInterval(.hours(-12)) + cell.datePicker.maximumDate = date.addingTimeInterval(LoopConstants.maxCarbEntryFutureTime) + cell.datePicker.minimumDate = date.addingTimeInterval(LoopConstants.maxCarbEntryPastTime) cell.datePicker.minuteInterval = 1 cell.date = date cell.delegate = self diff --git a/LoopTests/Managers/LoopDataManagerDosingTests.swift b/LoopTests/Managers/LoopDataManagerDosingTests.swift index 7506ba83f8..1a5cc54091 100644 --- a/LoopTests/Managers/LoopDataManagerDosingTests.swift +++ b/LoopTests/Managers/LoopDataManagerDosingTests.swift @@ -27,6 +27,8 @@ class MockDelegate: LoopDataManagerDelegate { self.recommendation = automaticDose.recommendation completion(error) } + func loopDataManager(_ manager: Loop.LoopDataManager, loopDidFinishWithDosingDecision: LoopKit.StoredDosingDecision, error: Loop.LoopError?) { + } func roundBasalRate(unitsPerHour: Double) -> Double { Double(Int(unitsPerHour / 0.05)) * 0.05 } func roundBolusVolume(units: Double) -> Double { Double(Int(units / 0.05)) * 0.05 } var pumpManagerStatus: PumpManagerStatus? diff --git a/LoopTests/Models/Remote/BolusActionTests.swift b/LoopTests/Models/Remote/BolusActionTests.swift deleted file mode 100644 index 2129ea68b4..0000000000 --- a/LoopTests/Models/Remote/BolusActionTests.swift +++ /dev/null @@ -1,101 +0,0 @@ -// -// BolusActionTests.swift -// LoopKitTests -// -// Created by Bill Gestrich on 1/14/23. -// Copyright © 2023 LoopKit Authors. All rights reserved. -// - -import XCTest -@testable import Loop -import LoopKit - -final class BolusActionTests: XCTestCase { - - override func setUpWithError() throws { - } - - override func tearDownWithError() throws { - } - - func testToValidBolusAtMaxAmount_Succeeds() throws { - - //Arrange - let maxBolusAmount = 10.0 - let bolusAmount = maxBolusAmount - let action = BolusAction(amountInUnits: bolusAmount) - - //Act - let validatedBolusAmount = try action.toValidBolusAmount(maximumBolus: 10.0) - - //Assert - XCTAssertEqual(validatedBolusAmount, bolusAmount) - - } - - func testToValidBolusAmount_AboveMaxAmount_Fails() throws { - - //Arrange - let maxBolusAmount = 10.0 - let bolusAmount = maxBolusAmount + 0.1 - let action = BolusAction(amountInUnits: bolusAmount) - - //Act - var thrownError: Error? = nil - do { - let _ = try action.toValidBolusAmount(maximumBolus: maxBolusAmount) - } catch { - thrownError = error - } - - //Assert - guard let validationError = thrownError as? BolusActionError, case .exceedsMaxBolus = validationError else { - XCTFail("Unexpected type \(thrownError.debugDescription)") - return - } - } - - func testToValidBolusAmount_AtZero_Fails() throws { - - //Arrange - let bolusAmount = 0.0 - let action = BolusAction(amountInUnits: bolusAmount) - - //Act - var thrownError: Error? = nil - do { - let _ = try action.toValidBolusAmount(maximumBolus: 10.0) - } catch { - thrownError = error - } - - //Assert - guard let validationError = thrownError as? BolusActionError, case .invalidBolus = validationError else { - XCTFail("Unexpected type \(thrownError.debugDescription)") - return - } - } - - func testToValidBolusAmount_NegativeAmount_Fails() throws { - - //Arrange - let bolusAmount = -1.0 - let action = BolusAction(amountInUnits: bolusAmount) - - //Act - var thrownError: Error? = nil - do { - let _ = try action.toValidBolusAmount(maximumBolus: 10.0) - } catch { - thrownError = error - } - - //Assert - guard let validationError = thrownError as? BolusActionError, case .invalidBolus = validationError else { - XCTFail("Unexpected type \(thrownError.debugDescription)") - return - } - } - - -} diff --git a/LoopTests/Models/Remote/CarbActionTests.swift b/LoopTests/Models/Remote/CarbActionTests.swift deleted file mode 100644 index 277051252a..0000000000 --- a/LoopTests/Models/Remote/CarbActionTests.swift +++ /dev/null @@ -1,402 +0,0 @@ -// -// CarbActionTests.swift -// LoopTests -// -// Created by Bill Gestrich on 1/14/23. -// Copyright © 2022 LoopKit Authors. All rights reserved. -// - -import XCTest -import HealthKit -@testable import Loop -import LoopKit - -class CarbActionTests: XCTestCase { - - override func setUpWithError() throws { - } - - override func tearDownWithError() throws { - } - - - func testToValidCarbEntry_Succeeds() throws { - - //Arrange - let expectedCarbsInGrams = 15.0 - let expectedDate = Date() - let expectedAbsorptionTime = TimeInterval(hours: 4.0) - let foodType = "🍕" - - let action = CarbAction(amountInGrams: expectedCarbsInGrams, absorptionTime: expectedAbsorptionTime, foodType: foodType, startDate: expectedDate) - - //Act - let carbEntry = try action.toValidCarbEntry(defaultAbsorptionTime: TimeInterval(hours: 3.0), - minAbsorptionTime: TimeInterval(hours: 0.5), - maxAbsorptionTime: TimeInterval(hours: 5.0), - maxCarbEntryQuantity: expectedCarbsInGrams, - maxCarbEntryPastTime: .hours(-12), - maxCarbEntryFutureTime: .hours(1) - ) - - //Assert - XCTAssertEqual(carbEntry.startDate, expectedDate) - XCTAssertEqual(carbEntry.absorptionTime, expectedAbsorptionTime) - XCTAssertEqual(carbEntry.quantity, HKQuantity(unit: .gram(), doubleValue: expectedCarbsInGrams)) - XCTAssertEqual(carbEntry.foodType, foodType) - } - - func testToValidCarbEntry_MissingAbsorptionHours_UsesDefaultAbsorption() throws { - - //Arrange - let defaultAbsorptionTime = TimeInterval(hours: 4.0) - let action = CarbAction(amountInGrams: 15.0, startDate: Date()) - - //Act - let carbEntry = try action.toValidCarbEntry(defaultAbsorptionTime: defaultAbsorptionTime, - minAbsorptionTime: TimeInterval(hours: 0.5), - maxAbsorptionTime: TimeInterval(hours: 5.0), - maxCarbEntryQuantity: 200, - maxCarbEntryPastTime: .hours(-12), - maxCarbEntryFutureTime: .hours(1)) - - //Assert - XCTAssertEqual(carbEntry.absorptionTime, defaultAbsorptionTime) - } - - func testToValidCarbEntry_AtMinAbsorptionHours_Succeeds() throws { - - //Arrange - let minAbsorptionTime = TimeInterval(hours: 0.5) - let action = CarbAction(amountInGrams: 15.0, - absorptionTime: minAbsorptionTime, - startDate: Date()) - - //Act - let carbEntry = try action.toValidCarbEntry(defaultAbsorptionTime: minAbsorptionTime, - minAbsorptionTime: minAbsorptionTime, - maxAbsorptionTime: TimeInterval(hours: 5.0), - maxCarbEntryQuantity: 200, - maxCarbEntryPastTime: .hours(-12), - maxCarbEntryFutureTime: .hours(1)) - - //Assert - XCTAssertEqual(carbEntry.absorptionTime, minAbsorptionTime) - } - - func testToValidCarbEntry_BelowMinAbsorptionHours_Fails() throws { - - //Arrange - let minAbsorptionTime = TimeInterval(hours: 0.5) - let aborptionOverrideTime = TimeInterval(hours: 0.4) - let action = CarbAction(amountInGrams: 15.0, - absorptionTime: aborptionOverrideTime, - startDate: Date()) - - //Act - var thrownError: Error? = nil - do { - let _ = try action.toValidCarbEntry(defaultAbsorptionTime: minAbsorptionTime, - minAbsorptionTime: minAbsorptionTime, - maxAbsorptionTime: TimeInterval(hours: 5.0), - maxCarbEntryQuantity: 200, - maxCarbEntryPastTime: .hours(-12), - maxCarbEntryFutureTime: .hours(1) - ) - } catch { - thrownError = error - } - - //Assert - guard let validationError = thrownError as? CarbActionError, case .invalidAbsorptionTime = validationError else { - XCTFail("Unexpected type \(thrownError.debugDescription)") - return - } - } - - func testToValidCarbEntry_AtMaxAbsorptionHours_Succeeds() throws { - - //Arrange - let maxAbsorptionTime = TimeInterval(hours: 5.0) - let action = CarbAction(amountInGrams: 15.0, - absorptionTime: maxAbsorptionTime, - startDate: Date()) - - //Act - let carbEntry = try action.toValidCarbEntry(defaultAbsorptionTime: maxAbsorptionTime, - minAbsorptionTime: TimeInterval(hours: 0.5), - maxAbsorptionTime: maxAbsorptionTime, - maxCarbEntryQuantity: 200, - maxCarbEntryPastTime: .hours(-12), - maxCarbEntryFutureTime: .hours(1) - ) - - //Assert - XCTAssertEqual(carbEntry.absorptionTime, maxAbsorptionTime) - } - - func testToValidCarbEntry_AboveMaxAbsorptionHours_Fails() throws { - - //Arrange - let maxAbsorptionTime = TimeInterval(hours: 5.0) - let absorptionTime = TimeInterval(hours: 5.1) - let action = CarbAction(amountInGrams: 15.0, - absorptionTime: absorptionTime, - startDate: Date()) - - //Act - var thrownError: Error? = nil - do { - let _ = try action.toValidCarbEntry(defaultAbsorptionTime: maxAbsorptionTime, - minAbsorptionTime: TimeInterval(hours: 0.5), - maxAbsorptionTime: maxAbsorptionTime, - maxCarbEntryQuantity: 200, - maxCarbEntryPastTime: .hours(-12), - maxCarbEntryFutureTime: .hours(1) - ) - } catch { - thrownError = error - } - - //Assert - guard let validationError = thrownError as? CarbActionError, case .invalidAbsorptionTime = validationError else { - XCTFail("Unexpected type \(thrownError.debugDescription)") - return - } - } - - func testToValidCarbEntry_AtMinStartTime_Succeeds() throws { - - //Arrange - let maxCarbEntryPastTime = TimeInterval(hours: -12) - let nowDate = Date() - let startDate = nowDate.addingTimeInterval(maxCarbEntryPastTime) - let action = CarbAction(amountInGrams: 15.0, - absorptionTime: TimeInterval(hours: 5.0), - startDate: startDate) - - //Act - let carbEntry = try action.toValidCarbEntry(defaultAbsorptionTime: TimeInterval(hours: 3.0), - minAbsorptionTime: TimeInterval(hours: 0.5), - maxAbsorptionTime: TimeInterval(hours: 5.0), - maxCarbEntryQuantity: 200, - maxCarbEntryPastTime: maxCarbEntryPastTime, - maxCarbEntryFutureTime: .hours(1), - nowDate: nowDate - ) - - //Assert - XCTAssertEqual(carbEntry.startDate, startDate) - } - - func testToValidCarbEntry_BeforeMinStartTime_Fails() throws { - - //Arrange - let maxCarbEntryPastTime = TimeInterval(hours: -12) - let nowDate = Date() - let startDate = nowDate.addingTimeInterval(maxCarbEntryPastTime - 1) - let action = CarbAction(amountInGrams: 15.0, - absorptionTime: TimeInterval(hours: 5.0), - startDate: startDate) - - //Act - var thrownError: Error? = nil - do { - let _ = try action.toValidCarbEntry(defaultAbsorptionTime: TimeInterval(hours: 3.0), - minAbsorptionTime: TimeInterval(hours: 0.5), - maxAbsorptionTime: TimeInterval(hours: 5.0), - maxCarbEntryQuantity: 200, - maxCarbEntryPastTime: maxCarbEntryPastTime, - maxCarbEntryFutureTime: .hours(1) - ) - } catch { - thrownError = error - } - - //Assert - guard let validationError = thrownError as? CarbActionError, case .invalidStartDate = validationError else { - XCTFail("Unexpected type \(thrownError.debugDescription)") - return - } - } - - func testToValidCarbEntry_AtMaxStartTime_Succeeds() throws { - - //Arrange - let maxCarbEntryFutureTime = TimeInterval(hours: 1) - let nowDate = Date() - let startDate = nowDate.addingTimeInterval(maxCarbEntryFutureTime) - let action = CarbAction(amountInGrams: 15.0, - absorptionTime: TimeInterval(hours: 5.0), - startDate: startDate) - - //Act - let carbEntry = try action.toValidCarbEntry(defaultAbsorptionTime: TimeInterval(hours: 3.0), - minAbsorptionTime: TimeInterval(hours: 0.5), - maxAbsorptionTime: TimeInterval(hours: 5.0), - maxCarbEntryQuantity: 200, - maxCarbEntryPastTime: .hours(-12), - maxCarbEntryFutureTime: maxCarbEntryFutureTime, - nowDate: nowDate - ) - - //Assert - XCTAssertEqual(carbEntry.startDate, startDate) - } - - func testToValidCarbEntry_AfterMaxStartTime_Fails() throws { - - //Arrange - let maxCarbEntryFutureTime = TimeInterval(hours: 1) - let nowDate = Date() - let startDate = nowDate.addingTimeInterval(maxCarbEntryFutureTime + 1) - let action = CarbAction(amountInGrams: 15.0, - absorptionTime: TimeInterval(hours: 5.0), - startDate: startDate) - - //Act - var thrownError: Error? = nil - do { - let _ = try action.toValidCarbEntry(defaultAbsorptionTime: TimeInterval(hours: 3.0), - minAbsorptionTime: TimeInterval(hours: 0.5), - maxAbsorptionTime: TimeInterval(hours: 5.0), - maxCarbEntryQuantity: 200, - maxCarbEntryPastTime: .hours(-12), - maxCarbEntryFutureTime: maxCarbEntryFutureTime - ) - } catch { - thrownError = error - } - - //Assert - guard let validationError = thrownError as? CarbActionError, case .invalidStartDate = validationError else { - XCTFail("Unexpected type \(thrownError.debugDescription)") - return - } - } - - func testToValidCarbEntry_AtMaxCarbs_Succeeds() throws { - - let maxCarbsAmount = 200.0 - let carbsAmount = maxCarbsAmount - - //Arrange - let action = CarbAction(amountInGrams: carbsAmount, - absorptionTime: TimeInterval(hours: 5.0), - startDate: Date()) - - //Act - let carbEntry = try action.toValidCarbEntry(defaultAbsorptionTime: TimeInterval(hours: 3.0), - minAbsorptionTime: TimeInterval(hours: 0.5), - maxAbsorptionTime: TimeInterval(hours: 5.0), - maxCarbEntryQuantity: maxCarbsAmount, - maxCarbEntryPastTime: .hours(-12), - maxCarbEntryFutureTime: TimeInterval(hours: 1) - ) - - //Assert - XCTAssertEqual(carbEntry.quantity, HKQuantity(unit: .gram(), doubleValue: carbsAmount)) - } - - func testToValidCarbEntry_AboveMaxCarbs_Fails() throws { - - let maxCarbsAmount = 200.0 - let carbsAmount = maxCarbsAmount + 1 - - //Arrange - let action = CarbAction(amountInGrams: carbsAmount, - absorptionTime: TimeInterval(hours: 5.0), - startDate: Date()) - - //Act - var thrownError: Error? = nil - do { - let _ = try action.toValidCarbEntry(defaultAbsorptionTime: TimeInterval(hours: 3.0), - minAbsorptionTime: TimeInterval(hours: 0.5), - maxAbsorptionTime: TimeInterval(hours: 5.0), - maxCarbEntryQuantity: 200, - maxCarbEntryPastTime: .hours(-12), - maxCarbEntryFutureTime: .hours(1.0) - ) - } catch { - thrownError = error - } - - //Assert - guard let validationError = thrownError as? CarbActionError, case .exceedsMaxCarbs = validationError else { - XCTFail("Unexpected type \(thrownError.debugDescription)") - return - } - } - - func testToValidCarbEntry_NegativeCarbs_Fails() throws { - - let carbsAmount = -1.0 - - //Arrange - let action = CarbAction(amountInGrams: carbsAmount, - absorptionTime: TimeInterval(hours: 5.0), - startDate: Date()) - - //Act - var thrownError: Error? = nil - do { - let _ = try action.toValidCarbEntry(defaultAbsorptionTime: TimeInterval(hours: 3.0), - minAbsorptionTime: TimeInterval(hours: 0.5), - maxAbsorptionTime: TimeInterval(hours: 5.0), - maxCarbEntryQuantity: 200, - maxCarbEntryPastTime: .hours(-12), - maxCarbEntryFutureTime: .hours(1.0) - ) - } catch { - thrownError = error - } - - //Assert - guard let validationError = thrownError as? CarbActionError, case .invalidCarbs = validationError else { - XCTFail("Unexpected type \(thrownError.debugDescription)") - return - } - } - - func testToValidCarbEntry_ZeroCarbs_Fails() throws { - - let carbsAmount = 0.0 - - //Arrange - let action = CarbAction(amountInGrams: carbsAmount, - absorptionTime: TimeInterval(hours: 5.0), - startDate: Date()) - - //Act - var thrownError: Error? = nil - do { - let _ = try action.toValidCarbEntry(defaultAbsorptionTime: TimeInterval(hours: 3.0), - minAbsorptionTime: TimeInterval(hours: 0.5), - maxAbsorptionTime: TimeInterval(hours: 5.0), - maxCarbEntryQuantity: 200, - maxCarbEntryPastTime: .hours(-12), - maxCarbEntryFutureTime: .hours(1.0) - ) - } catch { - thrownError = error - } - - //Assert - guard let validationError = thrownError as? CarbActionError, case .invalidCarbs = validationError else { - XCTFail("Unexpected type \(thrownError.debugDescription)") - return - } - } - -} - - -//MARK: Utils - -func dateFormatter() -> ISO8601DateFormatter { - let formatter = ISO8601DateFormatter() - formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] - return formatter -} - diff --git a/LoopTests/Models/Remote/OverrideActionTests.swift b/LoopTests/Models/Remote/OverrideActionTests.swift deleted file mode 100644 index fe9f5d7fb3..0000000000 --- a/LoopTests/Models/Remote/OverrideActionTests.swift +++ /dev/null @@ -1,152 +0,0 @@ -// -// OverrideActionTests.swift -// LoopKitTests -// -// Created by Bill Gestrich on 1/14/23. -// Copyright © 2023 LoopKit Authors. All rights reserved. -// - -import XCTest -@testable import Loop -import LoopKit - -final class OverrideActionTests: XCTestCase { - - override func setUpWithError() throws { - - } - - override func tearDownWithError() throws { - - } - - func testToValidOverride_Succeeds() throws { - - //Arrange - let durationTime = TimeInterval(hours: 1.0) - let remoteAddress = "1234-54321" - let overrideName = "My-Override" - let action = OverrideAction(name: overrideName, durationTime: durationTime, remoteAddress: remoteAddress) - let presets = [TemporaryScheduleOverridePreset(symbol: "", name: overrideName, settings: .init(targetRange: .none), duration: .indefinite)] - - //Act - let validOverride = try action.toValidOverride(allowedPresets: presets) - - //Assert - XCTAssertEqual(validOverride.duration, .finite(durationTime)) - switch validOverride.enactTrigger { - case .remote(let triggerAddress): - XCTAssertEqual(triggerAddress, remoteAddress) - default: - XCTFail("Unexpected trigger trigger type") - } - } - - func testToValidOverride_WhenOverrideNotInPresets_Fails() throws { - - //Arrange - let action = OverrideAction(name: "Unknown-Override", durationTime: TimeInterval(hours: 1.0), remoteAddress: "1234-54321") - let presets = [TemporaryScheduleOverridePreset(symbol: "", name: "My-Override", settings: .init(targetRange: .none), duration: .indefinite)] - - //Act - var thrownError: Error? = nil - do { - let _ = try action.toValidOverride(allowedPresets: presets) - } catch { - thrownError = error - } - - //Assert - guard let validationError = thrownError as? OverrideActionError, case .unknownPreset = validationError else { - XCTFail("Unexpected type \(thrownError.debugDescription)") - return - } - } - - func testToValidOverride_WhenNoDuration_YieldsIndefiniteOverride() throws { - - //Arrange - let action = OverrideAction(name: "My-Override", durationTime: nil, remoteAddress: "1234-54321") - let presets = [TemporaryScheduleOverridePreset(symbol: "", name: "My-Override", settings: .init(targetRange: .none), duration: .indefinite)] - - //Act - let validOverride = try action.toValidOverride(allowedPresets: presets) - - //Assert - XCTAssertEqual(validOverride.duration, .indefinite) - } - - func testToValidOverride_WhenDurationZero_YieldsIndefiniteOverride() throws { - - //Arrange - let action = OverrideAction(name: "My-Override", durationTime: TimeInterval(hours: 0), remoteAddress: "1234-54321") - let presets = [TemporaryScheduleOverridePreset(symbol: "", name: "My-Override", settings: .init(targetRange: .none), duration: .indefinite)] - - //Act - let validOverride = try action.toValidOverride(allowedPresets: presets) - - //Assert - XCTAssertEqual(validOverride.duration, .indefinite) - } - - func testToValidOverride_WhenNegativeDuration_Fails() throws { - - //Arrange - let action = OverrideAction(name: "My-Override", durationTime: TimeInterval(hours: -1.0), remoteAddress: "1234-54321") - let presets = [TemporaryScheduleOverridePreset(symbol: "", name: "My-Override", settings: .init(targetRange: .none), duration: .indefinite)] - - //Act - var thrownError: Error? = nil - do { - let _ = try action.toValidOverride(allowedPresets: presets) - } catch { - thrownError = error - } - - //Assert - guard let validationError = thrownError as? OverrideActionError, case .negativeDuration = validationError else { - XCTFail("Unexpected type \(thrownError.debugDescription)") - return - } - } - - //Limit to 24 hour duration - - func testToValidOverride_WhenAtMaxDuration_Succeeds() throws { - - //Arrange - let duration = TimeInterval(hours: 24) - let action = OverrideAction(name: "My-Override", durationTime: duration, remoteAddress: "1234-54321") - let presets = [TemporaryScheduleOverridePreset(symbol: "", name: "My-Override", settings: .init(targetRange: .none), duration: .indefinite)] - - //Act - let validOverride = try action.toValidOverride(allowedPresets: presets) - - //Assert - XCTAssertEqual(validOverride.duration, .finite(duration)) - - } - - func testToValidOverride_WhenAtMaxDuration_Fails() throws { - - //Arrange - let duration = TimeInterval(hours: 24) + 1 - let action = OverrideAction(name: "My-Override", durationTime: duration, remoteAddress: "1234-54321") - let presets = [TemporaryScheduleOverridePreset(symbol: "", name: "My-Override", settings: .init(targetRange: .none), duration: .indefinite)] - - //Act - var thrownError: Error? = nil - do { - let _ = try action.toValidOverride(allowedPresets: presets) - } catch { - thrownError = error - } - - //Assert - guard let validationError = thrownError as? OverrideActionError, case .durationExceedsMax = validationError else { - XCTFail("Unexpected type \(thrownError.debugDescription)") - return - } - } - -} From 89f2293e9bad92dd33f44310878dc89cef91cc52 Mon Sep 17 00:00:00 2001 From: Bill Gestrich <3207996+gestrich@users.noreply.github.com> Date: Thu, 22 Jun 2023 17:50:08 -0400 Subject: [PATCH 02/15] Move remote background handling to ServicesManager. Use notification to detect loop completion. Move handleRemoteNotification to ServicesManager --- Loop/Managers/DeviceDataManager.swift | 62 -------------------------- Loop/Managers/LoopAppManager.swift | 2 +- Loop/Managers/LoopDataManager.swift | 10 ----- Loop/Managers/ServicesManager.swift | 63 +++++++++++++++++++++++++++ 4 files changed, 64 insertions(+), 73 deletions(-) diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index f8c906d972..66c0ce8ed8 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -1339,14 +1339,6 @@ extension DeviceDataManager: LoopDataManagerDelegate { self.crashRecoveryManager.dosingFinished() } } - - func loopDataManager( - _ manager: LoopDataManager, - loopDidFinishWithDosingDecision: - StoredDosingDecision, error: LoopError? - ) { - processPendingRemoteCommands() - } } @@ -1360,39 +1352,6 @@ extension Notification.Name { extension DeviceDataManager: RemoteActionDelegate { - func handleRemoteNotification(_ notification: [String: AnyObject]) { - Task { - log.default("Remote Notification: Handling notification %{public}@", notification) - - guard FeatureFlags.remoteCommandsEnabled else { - log.error("Remote Notification: Remote Commands not enabled.") - return - } - - let backgroundTask = await beginBackgroundTask(name: "Handle Remote Notification") - do { - try await remoteDataServicesManager.handleRemoteNotification(notification) - } catch { - log.error("Remote Notification: Error: %{public}@", String(describing: error)) - } - - await endBackgroundTask(backgroundTask) - log.default("Remote Notification: Finished handling") - } - } - - func processPendingRemoteCommands() { - Task { - guard FeatureFlags.remoteCommandsEnabled else { - return - } - - let backgroundTask = await beginBackgroundTask(name: "Handle Pending Remote Commands") - await remoteDataServicesManager.processPendingRemoteCommands() - await endBackgroundTask(backgroundTask) - } - } - //Remote Overrides func handleRemoteOverride(name: String, durationTime: TimeInterval?, remoteAddress: String) async throws { @@ -1587,27 +1546,6 @@ extension DeviceDataManager: RemoteActionDelegate { } } } - - //Background Uploads - - func beginBackgroundTask(name: String) async -> UIBackgroundTaskIdentifier? { - var backgroundTask: UIBackgroundTaskIdentifier? - backgroundTask = await UIApplication.shared.beginBackgroundTask(withName: name) { - guard let backgroundTask = backgroundTask else {return} - Task { - await UIApplication.shared.endBackgroundTask(backgroundTask) - } - - self.log.error("Background Task Expired: %{public}@", name) - } - - return backgroundTask - } - - func endBackgroundTask(_ backgroundTask: UIBackgroundTaskIdentifier?) async { - guard let backgroundTask else {return} - await UIApplication.shared.endBackgroundTask(backgroundTask) - } } // MARK: - Critical Event Log Export diff --git a/Loop/Managers/LoopAppManager.swift b/Loop/Managers/LoopAppManager.swift index 3ec8c27fe6..e0c7758097 100644 --- a/Loop/Managers/LoopAppManager.swift +++ b/Loop/Managers/LoopAppManager.swift @@ -344,7 +344,7 @@ class LoopAppManager: NSObject { guard let notification = notification else { return false } - deviceDataManager?.handleRemoteNotification(notification) + deviceDataManager?.servicesManager.handleRemoteNotification(notification) return true } diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index 0c5455d008..ffc66ee314 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -905,8 +905,6 @@ extension LoopDataManager { } updateRemoteRecommendation() - - self.delegate?.loopDataManager(self, loopDidFinishWithDosingDecision: dosingDecision, error: error) } fileprivate enum UpdateReason: String { @@ -2208,14 +2206,6 @@ protocol LoopDataManagerDelegate: AnyObject { /// - units: The recommended bolus in U /// - Returns: a supported bolus volume in U. The volume returned should be the nearest deliverable volume. func roundBolusVolume(units: Double) -> Double - - /// Informs the delegate that a Loop finished - /// - /// - Parameters: - /// - manager: The manager - /// - dosingDecision: The resulting dosing decision - /// - error: An error, if any - func loopDataManager(_ manager: LoopDataManager, loopDidFinishWithDosingDecision: StoredDosingDecision, error: LoopError?) /// The pump manager status, if one exists. var pumpManagerStatus: PumpManagerStatus? { get } diff --git a/Loop/Managers/ServicesManager.swift b/Loop/Managers/ServicesManager.swift index 0c9a78b5a4..0a1810aa23 100644 --- a/Loop/Managers/ServicesManager.swift +++ b/Loop/Managers/ServicesManager.swift @@ -10,6 +10,7 @@ import os.log import LoopKit import LoopKitUI import LoopCore +import Combine class ServicesManager { @@ -30,6 +31,8 @@ class ServicesManager { private let servicesLock = UnfairLock() private let log = OSLog(category: "ServicesManager") + + lazy private var cancellables = Set() @PersistedProperty(key: "Services") var rawServices: [Service.RawValue]? @@ -49,6 +52,14 @@ class ServicesManager { self.remoteDataServicesManager = remoteDataServicesManager self.remoteActionDelegate = remoteActionDelegate restoreState() + + NotificationCenter.default + .publisher(for: .LoopCompleted) + .receive(on: DispatchQueue.main) + .sink { [weak self] note in + self?.processPendingRemoteCommands() + } + .store(in: &cancellables) } public var availableServices: [ServiceDescriptor] { @@ -176,6 +187,58 @@ class ServicesManager { } } } + + func handleRemoteNotification(_ notification: [String: AnyObject]) { + Task { + log.default("Remote Notification: Handling notification %{public}@", notification) + + guard FeatureFlags.remoteCommandsEnabled else { + log.error("Remote Notification: Remote Commands not enabled.") + return + } + + let backgroundTask = await beginBackgroundTask(name: "Handle Remote Notification") + do { + try await remoteDataServicesManager.handleRemoteNotification(notification) + } catch { + log.error("Remote Notification: Error: %{public}@", String(describing: error)) + } + + await endBackgroundTask(backgroundTask) + log.default("Remote Notification: Finished handling") + } + } + + func processPendingRemoteCommands() { + Task { + guard FeatureFlags.remoteCommandsEnabled else { + return + } + + let backgroundTask = await beginBackgroundTask(name: "Handle Pending Remote Commands") + await remoteDataServicesManager.processPendingRemoteCommands() + await endBackgroundTask(backgroundTask) + } + } + + private func beginBackgroundTask(name: String) async -> UIBackgroundTaskIdentifier? { + var backgroundTask: UIBackgroundTaskIdentifier? + backgroundTask = await UIApplication.shared.beginBackgroundTask(withName: name) { + guard let backgroundTask = backgroundTask else {return} + Task { + await UIApplication.shared.endBackgroundTask(backgroundTask) + } + + self.log.error("Background Task Expired: %{public}@", name) + } + + return backgroundTask + } + + private func endBackgroundTask(_ backgroundTask: UIBackgroundTaskIdentifier?) async { + guard let backgroundTask else {return} + await UIApplication.shared.endBackgroundTask(backgroundTask) + } } // MARK: - ServiceDelegate From 36b0c9445ae2b633386aeb476fe78b6fb0e006ff Mon Sep 17 00:00:00 2001 From: Bill Gestrich <3207996+gestrich@users.noreply.github.com> Date: Thu, 22 Jun 2023 18:01:09 -0400 Subject: [PATCH 03/15] Add delegate for ServicesManager --- Loop/Managers/DeviceDataManager.swift | 4 ++-- Loop/Managers/ServicesManager.swift | 27 ++++++++++++++++++--------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 66c0ce8ed8..22cec655ac 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -416,7 +416,7 @@ final class DeviceDataManager { analyticsServicesManager: analyticsServicesManager, loggingServicesManager: loggingServicesManager, remoteDataServicesManager: remoteDataServicesManager, - remoteActionDelegate: self + servicesManagerDelegate: self ) let criticalEventLogs: [CriticalEventLog] = [settingsManager.settingsStore, glucoseStore, carbStore, dosingDecisionStore, doseStore, deviceLog, alertManager.alertStore] @@ -1350,7 +1350,7 @@ extension Notification.Name { // MARK: - Remote Notification Handling -extension DeviceDataManager: RemoteActionDelegate { +extension DeviceDataManager: ServicesManagerDelegate { //Remote Overrides diff --git a/Loop/Managers/ServicesManager.swift b/Loop/Managers/ServicesManager.swift index 0a1810aa23..86e3c515fb 100644 --- a/Loop/Managers/ServicesManager.swift +++ b/Loop/Managers/ServicesManager.swift @@ -24,7 +24,7 @@ class ServicesManager { let remoteDataServicesManager: RemoteDataServicesManager - weak var remoteActionDelegate: RemoteActionDelegate? + weak var servicesManagerDelegate: ServicesManagerDelegate? private var services = [Service]() @@ -43,14 +43,14 @@ class ServicesManager { analyticsServicesManager: AnalyticsServicesManager, loggingServicesManager: LoggingServicesManager, remoteDataServicesManager: RemoteDataServicesManager, - remoteActionDelegate: RemoteActionDelegate + servicesManagerDelegate: ServicesManagerDelegate ) { self.pluginManager = pluginManager self.alertManager = alertManager self.analyticsServicesManager = analyticsServicesManager self.loggingServicesManager = loggingServicesManager self.remoteDataServicesManager = remoteDataServicesManager - self.remoteActionDelegate = remoteActionDelegate + self.servicesManagerDelegate = servicesManagerDelegate restoreState() NotificationCenter.default @@ -241,6 +241,15 @@ class ServicesManager { } } +public protocol ServicesManagerDelegate: AnyObject { + func handleRemoteOverride(name: String, durationTime: TimeInterval?, remoteAddress: String) async throws + func handleRemoteOverrideCancel() async throws + func handleRemoteCarb(amountInGrams: Double, absorptionTime: TimeInterval?, foodType: String?, startDate: Date?) async throws + func handleRemoteBolus(amountInUnits: Double) async throws + func handleRemoteClosedLoop(activate: Bool) async throws + func handleRemoteAutobolus(activate: Bool) async throws +} + // MARK: - ServiceDelegate extension ServicesManager: ServiceDelegate { @@ -270,16 +279,16 @@ extension ServicesManager: ServiceDelegate { } func handleRemoteOverride(name: String, durationTime: TimeInterval?, remoteAddress: String) async throws { - try await remoteActionDelegate?.handleRemoteOverride(name: name, durationTime: durationTime, remoteAddress: remoteAddress) + try await servicesManagerDelegate?.handleRemoteOverride(name: name, durationTime: durationTime, remoteAddress: remoteAddress) } func handleRemoteOverrideCancel() async throws { - try await remoteActionDelegate?.handleRemoteOverrideCancel() + try await servicesManagerDelegate?.handleRemoteOverrideCancel() } func handleRemoteCarb(amountInGrams: Double, absorptionTime: TimeInterval?, foodType: String?, startDate: Date?) async throws { do { - try await remoteActionDelegate?.handleRemoteCarb(amountInGrams: amountInGrams, absorptionTime: absorptionTime, foodType: foodType, startDate: startDate) + try await servicesManagerDelegate?.handleRemoteCarb(amountInGrams: amountInGrams, absorptionTime: absorptionTime, foodType: foodType, startDate: startDate) await NotificationManager.sendRemoteCarbEntryNotification(amountInGrams: amountInGrams) } catch { await NotificationManager.sendRemoteCarbEntryFailureNotification(for: error, amountInGrams: amountInGrams) @@ -289,7 +298,7 @@ extension ServicesManager: ServiceDelegate { func handleRemoteBolus(amountInUnits: Double) async throws { do { - try await remoteActionDelegate?.handleRemoteBolus(amountInUnits: amountInUnits) + try await servicesManagerDelegate?.handleRemoteBolus(amountInUnits: amountInUnits) await NotificationManager.sendRemoteBolusNotification(amount: amountInUnits) } catch { await NotificationManager.sendRemoteBolusFailureNotification(for: error, amountInUnits: amountInUnits) @@ -298,11 +307,11 @@ extension ServicesManager: ServiceDelegate { } func handleRemoteClosedLoop(activate: Bool) async throws { - try await remoteActionDelegate?.handleRemoteClosedLoop(activate: activate) + try await servicesManagerDelegate?.handleRemoteClosedLoop(activate: activate) } func handleRemoteAutobolus(activate: Bool) async throws { - try await remoteActionDelegate?.handleRemoteAutobolus(activate: activate) + try await servicesManagerDelegate?.handleRemoteAutobolus(activate: activate) } } From fb6a37535797606fb6206596cdcfe2ee94ea3b8b Mon Sep 17 00:00:00 2001 From: Bill Gestrich <3207996+gestrich@users.noreply.github.com> Date: Thu, 22 Jun 2023 18:07:57 -0400 Subject: [PATCH 04/15] Remove remote details from DeviceDataManager --- Loop/Managers/DeviceDataManager.swift | 28 +++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 22cec655ac..cc216cea4c 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -1348,11 +1348,11 @@ extension Notification.Name { static let PumpEventsAdded = Notification.Name(rawValue: "com.loopKit.notification.PumpEventsAdded") } -// MARK: - Remote Notification Handling +// MARK: - ServicesManagerDelegate extension DeviceDataManager: ServicesManagerDelegate { - //Remote Overrides + //Overrides func handleRemoteOverride(name: String, durationTime: TimeInterval?, remoteAddress: String) async throws { @@ -1401,16 +1401,16 @@ extension DeviceDataManager: ServicesManagerDelegate { var errorDescription: String? { switch self { case .unknownPreset(let presetName): - return String(format: NSLocalizedString("Unknown preset: %1$@", comment: "Remote command error description: unknown preset (1: preset name)."), presetName) + return String(format: NSLocalizedString("Unknown preset: %1$@", comment: "Override error description: unknown preset (1: preset name)."), presetName) case .durationExceedsMax(let maxDurationTime): - return String(format: NSLocalizedString("Duration exceeds: %1$.1f hours", comment: "Remote command error description: duration exceed max (1: max duration in hours)."), maxDurationTime.hours) + return String(format: NSLocalizedString("Duration exceeds: %1$.1f hours", comment: "Override error description: duration exceed max (1: max duration in hours)."), maxDurationTime.hours) case .negativeDuration: - return String(format: NSLocalizedString("Negative duration not allowed", comment: "Remote command error description: negative duration error.")) + return String(format: NSLocalizedString("Negative duration not allowed", comment: "Override error description: negative duration error.")) } } } - //Remote Bolus + //Bolus func handleRemoteBolus(amountInUnits: Double) async throws { @@ -1440,16 +1440,16 @@ extension DeviceDataManager: ServicesManagerDelegate { var errorDescription: String? { switch self { case .invalidBolus: - return NSLocalizedString("Invalid Bolus Amount", comment: "Remote command error description: invalid bolus amount.") + return NSLocalizedString("Invalid Bolus Amount", comment: "Bolus error description: invalid bolus amount.") case .missingMaxBolus: - return NSLocalizedString("Missing maximum allowed bolus in settings", comment: "Remote command error description: missing maximum bolus in settings.") + return NSLocalizedString("Missing maximum allowed bolus in settings", comment: "Bolus error description: missing maximum bolus in settings.") case .exceedsMaxBolus: - return NSLocalizedString("Exceeds maximum allowed bolus in settings", comment: "Remote command error description: bolus exceeds maximum bolus in settings.") + return NSLocalizedString("Exceeds maximum allowed bolus in settings", comment: "Bolus error description: bolus exceeds maximum bolus in settings.") } } } - //Remote Carb Entry + //Carb Entry func handleRemoteCarb(amountInGrams: Double, absorptionTime: TimeInterval?, foodType: String?, startDate: Date?) async throws { @@ -1491,15 +1491,15 @@ extension DeviceDataManager: ServicesManagerDelegate { var errorDescription: String? { switch self { case .exceedsMaxCarbs: - return NSLocalizedString("Exceeds maximum allowed carbs", comment: "Remote command error description: carbs exceed maximum amount.") + return NSLocalizedString("Exceeds maximum allowed carbs", comment: "Carb error description: carbs exceed maximum amount.") case .invalidCarbs: - return NSLocalizedString("Invalid carb amount", comment: "Remote command error description: invalid carb amount.") + return NSLocalizedString("Invalid carb amount", comment: "Carb error description: invalid carb amount.") case .invalidAbsorptionTime(let absorptionTime): let absorptionHoursFormatted = Self.numberFormatter.string(from: absorptionTime.hours) ?? "" - return String(format: NSLocalizedString("Invalid absorption time: %1$@ hours", comment: "Remote command error description: invalid absorption time. (1: Input duration in hours)."), absorptionHoursFormatted) + return String(format: NSLocalizedString("Invalid absorption time: %1$@ hours", comment: "Carb error description: invalid absorption time. (1: Input duration in hours)."), absorptionHoursFormatted) case .invalidStartDate(let startDate): let startDateFormatted = Self.dateFormatter.string(from: startDate) - return String(format: NSLocalizedString("Start time is out of range: %@", comment: "Remote command error description: invalid start time is out of range."), startDateFormatted) + return String(format: NSLocalizedString("Start time is out of range: %@", comment: "Carb error description: invalid start time is out of range."), startDateFormatted) } } From e3a2627d881ef38c542756ae545d3e457aa49f3b Mon Sep 17 00:00:00 2001 From: Bill Gestrich <3207996+gestrich@users.noreply.github.com> Date: Thu, 22 Jun 2023 18:28:43 -0400 Subject: [PATCH 05/15] Remove remote terms from ServicesManagerDelegate api --- Loop/Managers/DeviceDataManager.swift | 24 ++++++++++++------------ Loop/Managers/ServicesManager.swift | 24 ++++++++++++------------ 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index cc216cea4c..a75e5b0142 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -1354,7 +1354,7 @@ extension DeviceDataManager: ServicesManagerDelegate { //Overrides - func handleRemoteOverride(name: String, durationTime: TimeInterval?, remoteAddress: String) async throws { + func updateOverrideSetting(name: String, durationTime: TimeInterval?, remoteAddress: String) async throws { guard let preset = loopManager.settings.overridePresets.first(where: { $0.name == name }) else { throw OverrideActionError.unknownPreset(name) @@ -1379,16 +1379,16 @@ extension DeviceDataManager: ServicesManagerDelegate { } } - await activateRemoteOverride(remoteOverride) + await updateOverrideSetting(remoteOverride) } - func handleRemoteOverrideCancel() async throws { - await activateRemoteOverride(nil) + func cancelCurrentOverride() async throws { + await updateOverrideSetting(nil) } - func activateRemoteOverride(_ remoteOverride: TemporaryScheduleOverride?) async { - loopManager.mutateSettings { settings in settings.scheduleOverride = remoteOverride } + func updateOverrideSetting(_ override: TemporaryScheduleOverride?) async { + loopManager.mutateSettings { settings in settings.scheduleOverride = override } await remoteDataServicesManager.triggerUpload(for: .overrides) } @@ -1412,7 +1412,7 @@ extension DeviceDataManager: ServicesManagerDelegate { //Bolus - func handleRemoteBolus(amountInUnits: Double) async throws { + func deliverBolus(amountInUnits: Double) async throws { guard amountInUnits > 0 else { throw BolusActionError.invalidBolus @@ -1451,7 +1451,7 @@ extension DeviceDataManager: ServicesManagerDelegate { //Carb Entry - func handleRemoteCarb(amountInGrams: Double, absorptionTime: TimeInterval?, foodType: String?, startDate: Date?) async throws { + func deliverCarbs(amountInGrams: Double, absorptionTime: TimeInterval?, foodType: String?, startDate: Date?) async throws { let absorptionTime = absorptionTime ?? carbStore.defaultAbsorptionTimes.medium if absorptionTime < LoopConstants.minCarbAbsorptionTime || absorptionTime > LoopConstants.maxCarbAbsorptionTime { @@ -1477,7 +1477,7 @@ extension DeviceDataManager: ServicesManagerDelegate { let quantity = HKQuantity(unit: .gram(), doubleValue: amountInGrams) let candidateCarbEntry = NewCarbEntry(quantity: quantity, startDate: startDate ?? Date(), foodType: foodType, absorptionTime: absorptionTime) - let _ = try await addRemoteCarbEntry(candidateCarbEntry) + let _ = try await devliverCarbEntry(candidateCarbEntry) await remoteDataServicesManager.triggerUpload(for: .carb) } @@ -1518,7 +1518,7 @@ extension DeviceDataManager: ServicesManagerDelegate { //Remote Autobolus Update - func handleRemoteAutobolus(activate: Bool) async throws { + func updateAutobolusSetting(activate: Bool) async throws { loopManager.mutateSettings { settings in settings.automaticDosingStrategy = activate ? .automaticBolus : .tempBasalOnly } @@ -1526,14 +1526,14 @@ extension DeviceDataManager: ServicesManagerDelegate { //Remote Closed Loop Update - func handleRemoteClosedLoop(activate: Bool) async throws { + func updateClosedLoopSetting(activate: Bool) async throws { loopManager.mutateSettings { settings in settings.dosingEnabled = activate } } //Can't add this concurrency wrapper method to LoopKit due to the minimum iOS version - func addRemoteCarbEntry(_ carbEntry: NewCarbEntry) async throws -> StoredCarbEntry { + func devliverCarbEntry(_ carbEntry: NewCarbEntry) async throws -> StoredCarbEntry { return try await withCheckedThrowingContinuation { continuation in carbStore.addCarbEntry(carbEntry) { result in switch result { diff --git a/Loop/Managers/ServicesManager.swift b/Loop/Managers/ServicesManager.swift index 86e3c515fb..a48af355a3 100644 --- a/Loop/Managers/ServicesManager.swift +++ b/Loop/Managers/ServicesManager.swift @@ -242,12 +242,12 @@ class ServicesManager { } public protocol ServicesManagerDelegate: AnyObject { - func handleRemoteOverride(name: String, durationTime: TimeInterval?, remoteAddress: String) async throws - func handleRemoteOverrideCancel() async throws - func handleRemoteCarb(amountInGrams: Double, absorptionTime: TimeInterval?, foodType: String?, startDate: Date?) async throws - func handleRemoteBolus(amountInUnits: Double) async throws - func handleRemoteClosedLoop(activate: Bool) async throws - func handleRemoteAutobolus(activate: Bool) async throws + func updateOverrideSetting(name: String, durationTime: TimeInterval?, remoteAddress: String) async throws + func cancelCurrentOverride() async throws + func deliverCarbs(amountInGrams: Double, absorptionTime: TimeInterval?, foodType: String?, startDate: Date?) async throws + func deliverBolus(amountInUnits: Double) async throws + func updateClosedLoopSetting(activate: Bool) async throws + func updateAutobolusSetting(activate: Bool) async throws } // MARK: - ServiceDelegate @@ -279,16 +279,16 @@ extension ServicesManager: ServiceDelegate { } func handleRemoteOverride(name: String, durationTime: TimeInterval?, remoteAddress: String) async throws { - try await servicesManagerDelegate?.handleRemoteOverride(name: name, durationTime: durationTime, remoteAddress: remoteAddress) + try await servicesManagerDelegate?.updateOverrideSetting(name: name, durationTime: durationTime, remoteAddress: remoteAddress) } func handleRemoteOverrideCancel() async throws { - try await servicesManagerDelegate?.handleRemoteOverrideCancel() + try await servicesManagerDelegate?.cancelCurrentOverride() } func handleRemoteCarb(amountInGrams: Double, absorptionTime: TimeInterval?, foodType: String?, startDate: Date?) async throws { do { - try await servicesManagerDelegate?.handleRemoteCarb(amountInGrams: amountInGrams, absorptionTime: absorptionTime, foodType: foodType, startDate: startDate) + try await servicesManagerDelegate?.deliverCarbs(amountInGrams: amountInGrams, absorptionTime: absorptionTime, foodType: foodType, startDate: startDate) await NotificationManager.sendRemoteCarbEntryNotification(amountInGrams: amountInGrams) } catch { await NotificationManager.sendRemoteCarbEntryFailureNotification(for: error, amountInGrams: amountInGrams) @@ -298,7 +298,7 @@ extension ServicesManager: ServiceDelegate { func handleRemoteBolus(amountInUnits: Double) async throws { do { - try await servicesManagerDelegate?.handleRemoteBolus(amountInUnits: amountInUnits) + try await servicesManagerDelegate?.deliverBolus(amountInUnits: amountInUnits) await NotificationManager.sendRemoteBolusNotification(amount: amountInUnits) } catch { await NotificationManager.sendRemoteBolusFailureNotification(for: error, amountInUnits: amountInUnits) @@ -307,11 +307,11 @@ extension ServicesManager: ServiceDelegate { } func handleRemoteClosedLoop(activate: Bool) async throws { - try await servicesManagerDelegate?.handleRemoteClosedLoop(activate: activate) + try await servicesManagerDelegate?.updateClosedLoopSetting(activate: activate) } func handleRemoteAutobolus(activate: Bool) async throws { - try await servicesManagerDelegate?.handleRemoteAutobolus(activate: activate) + try await servicesManagerDelegate?.updateAutobolusSetting(activate: activate) } } From 47fa03b90e5ca518278b5949b5cbc72dce299387 Mon Sep 17 00:00:00 2001 From: Bill Gestrich <3207996+gestrich@users.noreply.github.com> Date: Thu, 22 Jun 2023 18:49:16 -0400 Subject: [PATCH 06/15] Respond only to loopFinished notifications --- Loop/Managers/ServicesManager.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Loop/Managers/ServicesManager.swift b/Loop/Managers/ServicesManager.swift index a48af355a3..0314f2e3c8 100644 --- a/Loop/Managers/ServicesManager.swift +++ b/Loop/Managers/ServicesManager.swift @@ -54,10 +54,16 @@ class ServicesManager { restoreState() NotificationCenter.default - .publisher(for: .LoopCompleted) + .publisher(for: .LoopDataUpdated) .receive(on: DispatchQueue.main) .sink { [weak self] note in - self?.processPendingRemoteCommands() + guard let rawContext = note.userInfo?[LoopDataManager.LoopUpdateContextKey] as? LoopDataManager.LoopUpdateContext.RawValue else { + return + } + let context = LoopDataManager.LoopUpdateContext(rawValue: rawContext) + if case context = LoopDataManager.LoopUpdateContext.loopFinished { + self?.processPendingRemoteCommands() + } } .store(in: &cancellables) } From 82b05a7855a64fd6ceca87161147fca4c70b71ed Mon Sep 17 00:00:00 2001 From: Bill Gestrich <3207996+gestrich@users.noreply.github.com> Date: Thu, 22 Jun 2023 18:50:56 -0400 Subject: [PATCH 07/15] Remove unused method --- LoopTests/Managers/LoopDataManagerDosingTests.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/LoopTests/Managers/LoopDataManagerDosingTests.swift b/LoopTests/Managers/LoopDataManagerDosingTests.swift index 1a5cc54091..7506ba83f8 100644 --- a/LoopTests/Managers/LoopDataManagerDosingTests.swift +++ b/LoopTests/Managers/LoopDataManagerDosingTests.swift @@ -27,8 +27,6 @@ class MockDelegate: LoopDataManagerDelegate { self.recommendation = automaticDose.recommendation completion(error) } - func loopDataManager(_ manager: Loop.LoopDataManager, loopDidFinishWithDosingDecision: LoopKit.StoredDosingDecision, error: Loop.LoopError?) { - } func roundBasalRate(unitsPerHour: Double) -> Double { Double(Int(unitsPerHour / 0.05)) * 0.05 } func roundBolusVolume(units: Double) -> Double { Double(Int(units / 0.05)) * 0.05 } var pumpManagerStatus: PumpManagerStatus? From c598114d23834daeb5f5ed20b2bb39b716c3482d Mon Sep 17 00:00:00 2001 From: Bill Gestrich <3207996+gestrich@users.noreply.github.com> Date: Fri, 23 Jun 2023 05:24:55 -0400 Subject: [PATCH 08/15] Rename method --- Loop/Managers/RemoteDataServicesManager.swift | 4 ++-- Loop/Managers/ServicesManager.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Loop/Managers/RemoteDataServicesManager.swift b/Loop/Managers/RemoteDataServicesManager.swift index 8f5d4d8586..d2e557e110 100644 --- a/Loop/Managers/RemoteDataServicesManager.swift +++ b/Loop/Managers/RemoteDataServicesManager.swift @@ -640,9 +640,9 @@ extension RemoteDataServicesManager { //Remote Commands extension RemoteDataServicesManager { - public func handleRemoteNotification(_ notification: [String: AnyObject]) async throws { + public func remoteNotificationWasReceived(_ notification: [String: AnyObject]) async throws { let service = try serviceForPushNotification(notification) - return try await service.handleRemoteNotification(notification) + return try await service.remoteNotificationWasReceived(notification) } func processPendingRemoteCommands() async { diff --git a/Loop/Managers/ServicesManager.swift b/Loop/Managers/ServicesManager.swift index 0314f2e3c8..71dfac0f9e 100644 --- a/Loop/Managers/ServicesManager.swift +++ b/Loop/Managers/ServicesManager.swift @@ -205,7 +205,7 @@ class ServicesManager { let backgroundTask = await beginBackgroundTask(name: "Handle Remote Notification") do { - try await remoteDataServicesManager.handleRemoteNotification(notification) + try await remoteDataServicesManager.remoteNotificationWasReceived(notification) } catch { log.error("Remote Notification: Error: %{public}@", String(describing: error)) } From 1826af8b439f9e5c8867705b8a5f7116aeb84c17 Mon Sep 17 00:00:00 2001 From: Bill Gestrich <3207996+gestrich@users.noreply.github.com> Date: Fri, 23 Jun 2023 05:39:27 -0400 Subject: [PATCH 09/15] Update names --- Loop/Managers/ServicesManager.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Loop/Managers/ServicesManager.swift b/Loop/Managers/ServicesManager.swift index 71dfac0f9e..97c4a4fad9 100644 --- a/Loop/Managers/ServicesManager.swift +++ b/Loop/Managers/ServicesManager.swift @@ -284,15 +284,15 @@ extension ServicesManager: ServiceDelegate { removeActiveService(service) } - func handleRemoteOverride(name: String, durationTime: TimeInterval?, remoteAddress: String) async throws { + func updateRemoteOverride(name: String, durationTime: TimeInterval?, remoteAddress: String) async throws { try await servicesManagerDelegate?.updateOverrideSetting(name: name, durationTime: durationTime, remoteAddress: remoteAddress) } - func handleRemoteOverrideCancel() async throws { + func cancelRemoteOverride() async throws { try await servicesManagerDelegate?.cancelCurrentOverride() } - func handleRemoteCarb(amountInGrams: Double, absorptionTime: TimeInterval?, foodType: String?, startDate: Date?) async throws { + func deliverRemoteCarbs(amountInGrams: Double, absorptionTime: TimeInterval?, foodType: String?, startDate: Date?) async throws { do { try await servicesManagerDelegate?.deliverCarbs(amountInGrams: amountInGrams, absorptionTime: absorptionTime, foodType: foodType, startDate: startDate) await NotificationManager.sendRemoteCarbEntryNotification(amountInGrams: amountInGrams) @@ -302,7 +302,7 @@ extension ServicesManager: ServiceDelegate { } } - func handleRemoteBolus(amountInUnits: Double) async throws { + func deliverRemoteBolus(amountInUnits: Double) async throws { do { try await servicesManagerDelegate?.deliverBolus(amountInUnits: amountInUnits) await NotificationManager.sendRemoteBolusNotification(amount: amountInUnits) @@ -312,11 +312,11 @@ extension ServicesManager: ServiceDelegate { } } - func handleRemoteClosedLoop(activate: Bool) async throws { + func updateRemoteClosedLoop(activate: Bool) async throws { try await servicesManagerDelegate?.updateClosedLoopSetting(activate: activate) } - func handleRemoteAutobolus(activate: Bool) async throws { + func updateRemoteAutobolus(activate: Bool) async throws { try await servicesManagerDelegate?.updateAutobolusSetting(activate: activate) } } From deb021ecb9dcd1634eb312321a648b77a022ef60 Mon Sep 17 00:00:00 2001 From: Bill Gestrich <3207996+gestrich@users.noreply.github.com> Date: Fri, 23 Jun 2023 05:48:47 -0400 Subject: [PATCH 10/15] Update names --- Loop/Managers/RemoteDataServicesManager.swift | 4 ++-- Loop/Managers/ServicesManager.swift | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Loop/Managers/RemoteDataServicesManager.swift b/Loop/Managers/RemoteDataServicesManager.swift index d2e557e110..0d11fef70b 100644 --- a/Loop/Managers/RemoteDataServicesManager.swift +++ b/Loop/Managers/RemoteDataServicesManager.swift @@ -645,10 +645,10 @@ extension RemoteDataServicesManager { return try await service.remoteNotificationWasReceived(notification) } - func processPendingRemoteCommands() async { + func loopDidComplete() async { for service in remoteDataServices { do { - try await service.processPendingRemoteCommands() + try await service.loopDidComplete() } catch { self.log.error("Error fetching pending commands: %{public}@", String(describing: error)) } diff --git a/Loop/Managers/ServicesManager.swift b/Loop/Managers/ServicesManager.swift index 97c4a4fad9..94b75e8045 100644 --- a/Loop/Managers/ServicesManager.swift +++ b/Loop/Managers/ServicesManager.swift @@ -62,7 +62,7 @@ class ServicesManager { } let context = LoopDataManager.LoopUpdateContext(rawValue: rawContext) if case context = LoopDataManager.LoopUpdateContext.loopFinished { - self?.processPendingRemoteCommands() + self?.loopDidComplete() } } .store(in: &cancellables) @@ -215,14 +215,14 @@ class ServicesManager { } } - func processPendingRemoteCommands() { + func loopDidComplete() { Task { guard FeatureFlags.remoteCommandsEnabled else { return } let backgroundTask = await beginBackgroundTask(name: "Handle Pending Remote Commands") - await remoteDataServicesManager.processPendingRemoteCommands() + await remoteDataServicesManager.loopDidComplete() await endBackgroundTask(backgroundTask) } } From b4209dc224b01711dcb4c3440a9b8cced1bccfcd Mon Sep 17 00:00:00 2001 From: Bill Gestrich <3207996+gestrich@users.noreply.github.com> Date: Sat, 24 Jun 2023 09:00:29 -0400 Subject: [PATCH 11/15] Remove more remote references in DeviceDataManager --- Loop/Managers/DeviceDataManager.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index a75e5b0142..9a38df82a4 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -1516,7 +1516,7 @@ extension DeviceDataManager: ServicesManagerDelegate { }() } - //Remote Autobolus Update + //Autobolus Update func updateAutobolusSetting(activate: Bool) async throws { loopManager.mutateSettings { settings in @@ -1524,7 +1524,7 @@ extension DeviceDataManager: ServicesManagerDelegate { } } - //Remote Closed Loop Update + //Closed Loop Update func updateClosedLoopSetting(activate: Bool) async throws { loopManager.mutateSettings { settings in From cca2741a8bf795bb0d90fecf26dca3fc7b045600 Mon Sep 17 00:00:00 2001 From: Bill Gestrich <3207996+gestrich@users.noreply.github.com> Date: Sat, 24 Jun 2023 09:15:50 -0400 Subject: [PATCH 12/15] Remove Remote 2.0 parts --- Loop/Managers/DeviceDataManager.swift | 16 --------- Loop/Managers/RemoteDataServicesManager.swift | 10 ------ Loop/Managers/ServicesManager.swift | 36 ------------------- 3 files changed, 62 deletions(-) diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 9a38df82a4..f7888bae1d 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -1516,22 +1516,6 @@ extension DeviceDataManager: ServicesManagerDelegate { }() } - //Autobolus Update - - func updateAutobolusSetting(activate: Bool) async throws { - loopManager.mutateSettings { settings in - settings.automaticDosingStrategy = activate ? .automaticBolus : .tempBasalOnly - } - } - - //Closed Loop Update - - func updateClosedLoopSetting(activate: Bool) async throws { - loopManager.mutateSettings { settings in - settings.dosingEnabled = activate - } - } - //Can't add this concurrency wrapper method to LoopKit due to the minimum iOS version func devliverCarbEntry(_ carbEntry: NewCarbEntry) async throws -> StoredCarbEntry { return try await withCheckedThrowingContinuation { continuation in diff --git a/Loop/Managers/RemoteDataServicesManager.swift b/Loop/Managers/RemoteDataServicesManager.swift index 0d11fef70b..14a3416900 100644 --- a/Loop/Managers/RemoteDataServicesManager.swift +++ b/Loop/Managers/RemoteDataServicesManager.swift @@ -645,16 +645,6 @@ extension RemoteDataServicesManager { return try await service.remoteNotificationWasReceived(notification) } - func loopDidComplete() async { - for service in remoteDataServices { - do { - try await service.loopDidComplete() - } catch { - self.log.error("Error fetching pending commands: %{public}@", String(describing: error)) - } - } - } - func serviceForPushNotification(_ notification: [String: AnyObject]) throws -> RemoteDataService { let defaultServiceIdentifier = "NightscoutService" let serviceIdentifier = notification["serviceIdentifier"] as? String ?? defaultServiceIdentifier diff --git a/Loop/Managers/ServicesManager.swift b/Loop/Managers/ServicesManager.swift index 94b75e8045..c805584c0c 100644 --- a/Loop/Managers/ServicesManager.swift +++ b/Loop/Managers/ServicesManager.swift @@ -52,20 +52,6 @@ class ServicesManager { self.remoteDataServicesManager = remoteDataServicesManager self.servicesManagerDelegate = servicesManagerDelegate restoreState() - - NotificationCenter.default - .publisher(for: .LoopDataUpdated) - .receive(on: DispatchQueue.main) - .sink { [weak self] note in - guard let rawContext = note.userInfo?[LoopDataManager.LoopUpdateContextKey] as? LoopDataManager.LoopUpdateContext.RawValue else { - return - } - let context = LoopDataManager.LoopUpdateContext(rawValue: rawContext) - if case context = LoopDataManager.LoopUpdateContext.loopFinished { - self?.loopDidComplete() - } - } - .store(in: &cancellables) } public var availableServices: [ServiceDescriptor] { @@ -215,18 +201,6 @@ class ServicesManager { } } - func loopDidComplete() { - Task { - guard FeatureFlags.remoteCommandsEnabled else { - return - } - - let backgroundTask = await beginBackgroundTask(name: "Handle Pending Remote Commands") - await remoteDataServicesManager.loopDidComplete() - await endBackgroundTask(backgroundTask) - } - } - private func beginBackgroundTask(name: String) async -> UIBackgroundTaskIdentifier? { var backgroundTask: UIBackgroundTaskIdentifier? backgroundTask = await UIApplication.shared.beginBackgroundTask(withName: name) { @@ -252,8 +226,6 @@ public protocol ServicesManagerDelegate: AnyObject { func cancelCurrentOverride() async throws func deliverCarbs(amountInGrams: Double, absorptionTime: TimeInterval?, foodType: String?, startDate: Date?) async throws func deliverBolus(amountInUnits: Double) async throws - func updateClosedLoopSetting(activate: Bool) async throws - func updateAutobolusSetting(activate: Bool) async throws } // MARK: - ServiceDelegate @@ -311,14 +283,6 @@ extension ServicesManager: ServiceDelegate { throw error } } - - func updateRemoteClosedLoop(activate: Bool) async throws { - try await servicesManagerDelegate?.updateClosedLoopSetting(activate: activate) - } - - func updateRemoteAutobolus(activate: Bool) async throws { - try await servicesManagerDelegate?.updateAutobolusSetting(activate: activate) - } } extension ServicesManager: AlertIssuer { From 5988cdb8db6acc1c379bc0ad1bf154dacff705fd Mon Sep 17 00:00:00 2001 From: Bill Gestrich <3207996+gestrich@users.noreply.github.com> Date: Sat, 24 Jun 2023 19:44:32 -0400 Subject: [PATCH 13/15] Update enact override method name --- Loop/Managers/DeviceDataManager.swift | 8 ++++---- Loop/Managers/ServicesManager.swift | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index f7888bae1d..0b488e0f2e 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -1354,7 +1354,7 @@ extension DeviceDataManager: ServicesManagerDelegate { //Overrides - func updateOverrideSetting(name: String, durationTime: TimeInterval?, remoteAddress: String) async throws { + func enactOverride(name: String, durationTime: TimeInterval?, remoteAddress: String) async throws { guard let preset = loopManager.settings.overridePresets.first(where: { $0.name == name }) else { throw OverrideActionError.unknownPreset(name) @@ -1379,15 +1379,15 @@ extension DeviceDataManager: ServicesManagerDelegate { } } - await updateOverrideSetting(remoteOverride) + await enactOverride(remoteOverride) } func cancelCurrentOverride() async throws { - await updateOverrideSetting(nil) + await enactOverride(nil) } - func updateOverrideSetting(_ override: TemporaryScheduleOverride?) async { + func enactOverride(_ override: TemporaryScheduleOverride?) async { loopManager.mutateSettings { settings in settings.scheduleOverride = override } await remoteDataServicesManager.triggerUpload(for: .overrides) } diff --git a/Loop/Managers/ServicesManager.swift b/Loop/Managers/ServicesManager.swift index c805584c0c..011bdaa837 100644 --- a/Loop/Managers/ServicesManager.swift +++ b/Loop/Managers/ServicesManager.swift @@ -222,7 +222,7 @@ class ServicesManager { } public protocol ServicesManagerDelegate: AnyObject { - func updateOverrideSetting(name: String, durationTime: TimeInterval?, remoteAddress: String) async throws + func enactOverride(name: String, durationTime: TimeInterval?, remoteAddress: String) async throws func cancelCurrentOverride() async throws func deliverCarbs(amountInGrams: Double, absorptionTime: TimeInterval?, foodType: String?, startDate: Date?) async throws func deliverBolus(amountInUnits: Double) async throws @@ -256,8 +256,8 @@ extension ServicesManager: ServiceDelegate { removeActiveService(service) } - func updateRemoteOverride(name: String, durationTime: TimeInterval?, remoteAddress: String) async throws { - try await servicesManagerDelegate?.updateOverrideSetting(name: name, durationTime: durationTime, remoteAddress: remoteAddress) + func enactRemoteOverride(name: String, durationTime: TimeInterval?, remoteAddress: String) async throws { + try await servicesManagerDelegate?.enactOverride(name: name, durationTime: durationTime, remoteAddress: remoteAddress) } func cancelRemoteOverride() async throws { From 52f143e77a6a538f6545b8b869f697256378d531 Mon Sep 17 00:00:00 2001 From: Bill Gestrich <3207996+gestrich@users.noreply.github.com> Date: Sun, 25 Jun 2023 14:18:05 -0400 Subject: [PATCH 14/15] Move validation to ServicesManager. Move ServiceManager delegation to LoopDataManager --- Loop/Managers/DeviceDataManager.swift | 153 +------------------------- Loop/Managers/LoopDataManager.swift | 122 ++++++++++++++++++++ Loop/Managers/ServicesManager.swift | 56 +++++++++- 3 files changed, 178 insertions(+), 153 deletions(-) diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 0b488e0f2e..50d929cd71 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -416,7 +416,8 @@ final class DeviceDataManager { analyticsServicesManager: analyticsServicesManager, loggingServicesManager: loggingServicesManager, remoteDataServicesManager: remoteDataServicesManager, - servicesManagerDelegate: self + servicesManagerDelegate: loopManager, + servicesManagerDosingDelegate: self ) let criticalEventLogs: [CriticalEventLog] = [settingsManager.settingsStore, glucoseStore, carbStore, dosingDecisionStore, doseStore, deviceLog, alertManager.alertStore] @@ -1348,69 +1349,9 @@ extension Notification.Name { static let PumpEventsAdded = Notification.Name(rawValue: "com.loopKit.notification.PumpEventsAdded") } -// MARK: - ServicesManagerDelegate +// MARK: - ServicesManagerDosingDelegate -extension DeviceDataManager: ServicesManagerDelegate { - - //Overrides - - func enactOverride(name: String, durationTime: TimeInterval?, remoteAddress: String) async throws { - - guard let preset = loopManager.settings.overridePresets.first(where: { $0.name == name }) else { - throw OverrideActionError.unknownPreset(name) - } - - var remoteOverride = preset.createOverride(enactTrigger: .remote(remoteAddress)) - - if let durationTime = durationTime { - - guard durationTime <= LoopConstants.maxOverrideDurationTime else { - throw OverrideActionError.durationExceedsMax(LoopConstants.maxOverrideDurationTime) - } - - guard durationTime >= 0 else { - throw OverrideActionError.negativeDuration - } - - if durationTime == 0 { - remoteOverride.duration = .indefinite - } else { - remoteOverride.duration = .finite(durationTime) - } - } - - await enactOverride(remoteOverride) - } - - - func cancelCurrentOverride() async throws { - await enactOverride(nil) - } - - func enactOverride(_ override: TemporaryScheduleOverride?) async { - loopManager.mutateSettings { settings in settings.scheduleOverride = override } - await remoteDataServicesManager.triggerUpload(for: .overrides) - } - - enum OverrideActionError: LocalizedError { - - case unknownPreset(String) - case durationExceedsMax(TimeInterval) - case negativeDuration - - var errorDescription: String? { - switch self { - case .unknownPreset(let presetName): - return String(format: NSLocalizedString("Unknown preset: %1$@", comment: "Override error description: unknown preset (1: preset name)."), presetName) - case .durationExceedsMax(let maxDurationTime): - return String(format: NSLocalizedString("Duration exceeds: %1$.1f hours", comment: "Override error description: duration exceed max (1: max duration in hours)."), maxDurationTime.hours) - case .negativeDuration: - return String(format: NSLocalizedString("Negative duration not allowed", comment: "Override error description: negative duration error.")) - } - } - } - - //Bolus +extension DeviceDataManager: ServicesManagerDosingDelegate { func deliverBolus(amountInUnits: Double) async throws { @@ -1426,9 +1367,7 @@ extension DeviceDataManager: ServicesManagerDelegate { throw BolusActionError.exceedsMaxBolus } - try await self.enactBolus(units: amountInUnits, activationType: .manualNoRecommendation) - await remoteDataServicesManager.triggerUpload(for: .dose) - self.analyticsServicesManager.didBolus(source: "Remote", units: amountInUnits) + try await enactBolus(units: amountInUnits, activationType: .manualNoRecommendation) } enum BolusActionError: LocalizedError { @@ -1448,88 +1387,6 @@ extension DeviceDataManager: ServicesManagerDelegate { } } } - - //Carb Entry - - func deliverCarbs(amountInGrams: Double, absorptionTime: TimeInterval?, foodType: String?, startDate: Date?) async throws { - - let absorptionTime = absorptionTime ?? carbStore.defaultAbsorptionTimes.medium - if absorptionTime < LoopConstants.minCarbAbsorptionTime || absorptionTime > LoopConstants.maxCarbAbsorptionTime { - throw CarbActionError.invalidAbsorptionTime(absorptionTime) - } - - guard amountInGrams > 0.0 else { - throw CarbActionError.invalidCarbs - } - - guard amountInGrams <= LoopConstants.maxCarbEntryQuantity.doubleValue(for: .gram()) else { - throw CarbActionError.exceedsMaxCarbs - } - - if let startDate = startDate { - let maxStartDate = Date().addingTimeInterval(LoopConstants.maxCarbEntryFutureTime) - let minStartDate = Date().addingTimeInterval(LoopConstants.maxCarbEntryPastTime) - guard startDate <= maxStartDate && startDate >= minStartDate else { - throw CarbActionError.invalidStartDate(startDate) - } - } - - let quantity = HKQuantity(unit: .gram(), doubleValue: amountInGrams) - let candidateCarbEntry = NewCarbEntry(quantity: quantity, startDate: startDate ?? Date(), foodType: foodType, absorptionTime: absorptionTime) - - let _ = try await devliverCarbEntry(candidateCarbEntry) - await remoteDataServicesManager.triggerUpload(for: .carb) - } - - enum CarbActionError: LocalizedError { - - case invalidAbsorptionTime(TimeInterval) - case invalidStartDate(Date) - case exceedsMaxCarbs - case invalidCarbs - - var errorDescription: String? { - switch self { - case .exceedsMaxCarbs: - return NSLocalizedString("Exceeds maximum allowed carbs", comment: "Carb error description: carbs exceed maximum amount.") - case .invalidCarbs: - return NSLocalizedString("Invalid carb amount", comment: "Carb error description: invalid carb amount.") - case .invalidAbsorptionTime(let absorptionTime): - let absorptionHoursFormatted = Self.numberFormatter.string(from: absorptionTime.hours) ?? "" - return String(format: NSLocalizedString("Invalid absorption time: %1$@ hours", comment: "Carb error description: invalid absorption time. (1: Input duration in hours)."), absorptionHoursFormatted) - case .invalidStartDate(let startDate): - let startDateFormatted = Self.dateFormatter.string(from: startDate) - return String(format: NSLocalizedString("Start time is out of range: %@", comment: "Carb error description: invalid start time is out of range."), startDateFormatted) - } - } - - static var numberFormatter: NumberFormatter = { - let formatter = NumberFormatter() - formatter.numberStyle = .decimal - return formatter - }() - - static var dateFormatter: DateFormatter = { - let formatter = DateFormatter() - formatter.timeStyle = .medium - return formatter - }() - } - - //Can't add this concurrency wrapper method to LoopKit due to the minimum iOS version - func devliverCarbEntry(_ carbEntry: NewCarbEntry) async throws -> StoredCarbEntry { - return try await withCheckedThrowingContinuation { continuation in - carbStore.addCarbEntry(carbEntry) { result in - switch result { - case .success(let storedCarbEntry): - self.analyticsServicesManager.didAddCarbs(source: "Remote", amount: carbEntry.quantity.doubleValue(for: .gram())) - continuation.resume(returning: storedCarbEntry) - case .failure(let error): - continuation.resume(throwing: error) - } - } - } - } } // MARK: - Critical Event Log Export diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index ffc66ee314..75359044c9 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -2350,3 +2350,125 @@ extension LoopDataManager { } } } + +extension LoopDataManager: ServicesManagerDelegate { + + //Overrides + + func enactOverride(name: String, duration: TemporaryScheduleOverride.Duration?, remoteAddress: String) async throws { + + guard let preset = settings.overridePresets.first(where: { $0.name == name }) else { + throw EnactOverrideError.unknownPreset(name) + } + + var remoteOverride = preset.createOverride(enactTrigger: .remote(remoteAddress)) + + if let duration { + remoteOverride.duration = duration + } + + await enactOverride(remoteOverride) + } + + + func cancelCurrentOverride() async throws { + await enactOverride(nil) + } + + func enactOverride(_ override: TemporaryScheduleOverride?) async { + mutateSettings { settings in settings.scheduleOverride = override } + } + + enum EnactOverrideError: LocalizedError { + + case unknownPreset(String) + + var errorDescription: String? { + switch self { + case .unknownPreset(let presetName): + return String(format: NSLocalizedString("Unknown preset: %1$@", comment: "Override error description: unknown preset (1: preset name)."), presetName) + } + } + } + + //Carb Entry + + func deliverCarbs(amountInGrams: Double, absorptionTime: TimeInterval?, foodType: String?, startDate: Date?) async throws { + + let absorptionTime = absorptionTime ?? carbStore.defaultAbsorptionTimes.medium + if absorptionTime < LoopConstants.minCarbAbsorptionTime || absorptionTime > LoopConstants.maxCarbAbsorptionTime { + throw CarbActionError.invalidAbsorptionTime(absorptionTime) + } + + guard amountInGrams > 0.0 else { + throw CarbActionError.invalidCarbs + } + + guard amountInGrams <= LoopConstants.maxCarbEntryQuantity.doubleValue(for: .gram()) else { + throw CarbActionError.exceedsMaxCarbs + } + + if let startDate = startDate { + let maxStartDate = Date().addingTimeInterval(LoopConstants.maxCarbEntryFutureTime) + let minStartDate = Date().addingTimeInterval(LoopConstants.maxCarbEntryPastTime) + guard startDate <= maxStartDate && startDate >= minStartDate else { + throw CarbActionError.invalidStartDate(startDate) + } + } + + let quantity = HKQuantity(unit: .gram(), doubleValue: amountInGrams) + let candidateCarbEntry = NewCarbEntry(quantity: quantity, startDate: startDate ?? Date(), foodType: foodType, absorptionTime: absorptionTime) + + let _ = try await devliverCarbEntry(candidateCarbEntry) + } + + enum CarbActionError: LocalizedError { + + case invalidAbsorptionTime(TimeInterval) + case invalidStartDate(Date) + case exceedsMaxCarbs + case invalidCarbs + + var errorDescription: String? { + switch self { + case .exceedsMaxCarbs: + return NSLocalizedString("Exceeds maximum allowed carbs", comment: "Carb error description: carbs exceed maximum amount.") + case .invalidCarbs: + return NSLocalizedString("Invalid carb amount", comment: "Carb error description: invalid carb amount.") + case .invalidAbsorptionTime(let absorptionTime): + let absorptionHoursFormatted = Self.numberFormatter.string(from: absorptionTime.hours) ?? "" + return String(format: NSLocalizedString("Invalid absorption time: %1$@ hours", comment: "Carb error description: invalid absorption time. (1: Input duration in hours)."), absorptionHoursFormatted) + case .invalidStartDate(let startDate): + let startDateFormatted = Self.dateFormatter.string(from: startDate) + return String(format: NSLocalizedString("Start time is out of range: %@", comment: "Carb error description: invalid start time is out of range."), startDateFormatted) + } + } + + static var numberFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + return formatter + }() + + static var dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.timeStyle = .medium + return formatter + }() + } + + //Can't add this concurrency wrapper method to LoopKit due to the minimum iOS version + func devliverCarbEntry(_ carbEntry: NewCarbEntry) async throws -> StoredCarbEntry { + return try await withCheckedThrowingContinuation { continuation in + carbStore.addCarbEntry(carbEntry) { result in + switch result { + case .success(let storedCarbEntry): + continuation.resume(returning: storedCarbEntry) + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + +} diff --git a/Loop/Managers/ServicesManager.swift b/Loop/Managers/ServicesManager.swift index 011bdaa837..ab027e719e 100644 --- a/Loop/Managers/ServicesManager.swift +++ b/Loop/Managers/ServicesManager.swift @@ -25,6 +25,7 @@ class ServicesManager { let remoteDataServicesManager: RemoteDataServicesManager weak var servicesManagerDelegate: ServicesManagerDelegate? + weak var servicesManagerDosingDelegate: ServicesManagerDosingDelegate? private var services = [Service]() @@ -43,7 +44,8 @@ class ServicesManager { analyticsServicesManager: AnalyticsServicesManager, loggingServicesManager: LoggingServicesManager, remoteDataServicesManager: RemoteDataServicesManager, - servicesManagerDelegate: ServicesManagerDelegate + servicesManagerDelegate: ServicesManagerDelegate, + servicesManagerDosingDelegate: ServicesManagerDosingDelegate ) { self.pluginManager = pluginManager self.alertManager = alertManager @@ -51,6 +53,7 @@ class ServicesManager { self.loggingServicesManager = loggingServicesManager self.remoteDataServicesManager = remoteDataServicesManager self.servicesManagerDelegate = servicesManagerDelegate + self.servicesManagerDosingDelegate = servicesManagerDosingDelegate restoreState() } @@ -221,11 +224,14 @@ class ServicesManager { } } +public protocol ServicesManagerDosingDelegate: AnyObject { + func deliverBolus(amountInUnits: Double) async throws +} + public protocol ServicesManagerDelegate: AnyObject { - func enactOverride(name: String, durationTime: TimeInterval?, remoteAddress: String) async throws + func enactOverride(name: String, duration: TemporaryScheduleOverride.Duration?, remoteAddress: String) async throws func cancelCurrentOverride() async throws func deliverCarbs(amountInGrams: Double, absorptionTime: TimeInterval?, foodType: String?, startDate: Date?) async throws - func deliverBolus(amountInUnits: Double) async throws } // MARK: - ServiceDelegate @@ -257,17 +263,55 @@ extension ServicesManager: ServiceDelegate { } func enactRemoteOverride(name: String, durationTime: TimeInterval?, remoteAddress: String) async throws { - try await servicesManagerDelegate?.enactOverride(name: name, durationTime: durationTime, remoteAddress: remoteAddress) + + var duration: TemporaryScheduleOverride.Duration? = nil + if let durationTime = durationTime { + + guard durationTime <= LoopConstants.maxOverrideDurationTime else { + throw OverrideActionError.durationExceedsMax(LoopConstants.maxOverrideDurationTime) + } + + guard durationTime >= 0 else { + throw OverrideActionError.negativeDuration + } + + if durationTime == 0 { + duration = .indefinite + } else { + duration = .finite(durationTime) + } + } + + try await servicesManagerDelegate?.enactOverride(name: name, duration: duration, remoteAddress: remoteAddress) + await remoteDataServicesManager.triggerUpload(for: .overrides) + } + + enum OverrideActionError: LocalizedError { + + case durationExceedsMax(TimeInterval) + case negativeDuration + + var errorDescription: String? { + switch self { + case .durationExceedsMax(let maxDurationTime): + return String(format: NSLocalizedString("Duration exceeds: %1$.1f hours", comment: "Override error description: duration exceed max (1: max duration in hours)."), maxDurationTime.hours) + case .negativeDuration: + return String(format: NSLocalizedString("Negative duration not allowed", comment: "Override error description: negative duration error.")) + } + } } func cancelRemoteOverride() async throws { try await servicesManagerDelegate?.cancelCurrentOverride() + await remoteDataServicesManager.triggerUpload(for: .overrides) } func deliverRemoteCarbs(amountInGrams: Double, absorptionTime: TimeInterval?, foodType: String?, startDate: Date?) async throws { do { try await servicesManagerDelegate?.deliverCarbs(amountInGrams: amountInGrams, absorptionTime: absorptionTime, foodType: foodType, startDate: startDate) await NotificationManager.sendRemoteCarbEntryNotification(amountInGrams: amountInGrams) + await remoteDataServicesManager.triggerUpload(for: .carb) + analyticsServicesManager.didAddCarbs(source: "Remote", amount: amountInGrams) } catch { await NotificationManager.sendRemoteCarbEntryFailureNotification(for: error, amountInGrams: amountInGrams) throw error @@ -276,8 +320,10 @@ extension ServicesManager: ServiceDelegate { func deliverRemoteBolus(amountInUnits: Double) async throws { do { - try await servicesManagerDelegate?.deliverBolus(amountInUnits: amountInUnits) + try await servicesManagerDosingDelegate?.deliverBolus(amountInUnits: amountInUnits) await NotificationManager.sendRemoteBolusNotification(amount: amountInUnits) + await remoteDataServicesManager.triggerUpload(for: .dose) + analyticsServicesManager.didBolus(source: "Remote", units: amountInUnits) } catch { await NotificationManager.sendRemoteBolusFailureNotification(for: error, amountInUnits: amountInUnits) throw error From b30bef95cabc74640da3964fd41157de98fd93ce Mon Sep 17 00:00:00 2001 From: Bill Gestrich <3207996+gestrich@users.noreply.github.com> Date: Fri, 30 Jun 2023 20:01:56 -0400 Subject: [PATCH 15/15] Move bolus validation to ServicesManager --- Loop/Managers/DeviceDataManager.swift | 31 +----------------------- Loop/Managers/ServicesManager.swift | 35 +++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 30 deletions(-) diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 50d929cd71..c3b11f82a7 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -416,6 +416,7 @@ final class DeviceDataManager { analyticsServicesManager: analyticsServicesManager, loggingServicesManager: loggingServicesManager, remoteDataServicesManager: remoteDataServicesManager, + settingsManager: settingsManager, servicesManagerDelegate: loopManager, servicesManagerDosingDelegate: self ) @@ -1354,39 +1355,9 @@ extension Notification.Name { extension DeviceDataManager: ServicesManagerDosingDelegate { func deliverBolus(amountInUnits: Double) async throws { - - guard amountInUnits > 0 else { - throw BolusActionError.invalidBolus - } - - guard let maxBolusAmount = loopManager.settings.maximumBolus else { - throw BolusActionError.missingMaxBolus - } - - guard amountInUnits <= maxBolusAmount else { - throw BolusActionError.exceedsMaxBolus - } - try await enactBolus(units: amountInUnits, activationType: .manualNoRecommendation) } - enum BolusActionError: LocalizedError { - - case invalidBolus - case missingMaxBolus - case exceedsMaxBolus - - var errorDescription: String? { - switch self { - case .invalidBolus: - return NSLocalizedString("Invalid Bolus Amount", comment: "Bolus error description: invalid bolus amount.") - case .missingMaxBolus: - return NSLocalizedString("Missing maximum allowed bolus in settings", comment: "Bolus error description: missing maximum bolus in settings.") - case .exceedsMaxBolus: - return NSLocalizedString("Exceeds maximum allowed bolus in settings", comment: "Bolus error description: bolus exceeds maximum bolus in settings.") - } - } - } } // MARK: - Critical Event Log Export diff --git a/Loop/Managers/ServicesManager.swift b/Loop/Managers/ServicesManager.swift index ab027e719e..2593560706 100644 --- a/Loop/Managers/ServicesManager.swift +++ b/Loop/Managers/ServicesManager.swift @@ -24,6 +24,8 @@ class ServicesManager { let remoteDataServicesManager: RemoteDataServicesManager + let settingsManager: SettingsManager + weak var servicesManagerDelegate: ServicesManagerDelegate? weak var servicesManagerDosingDelegate: ServicesManagerDosingDelegate? @@ -44,6 +46,7 @@ class ServicesManager { analyticsServicesManager: AnalyticsServicesManager, loggingServicesManager: LoggingServicesManager, remoteDataServicesManager: RemoteDataServicesManager, + settingsManager: SettingsManager, servicesManagerDelegate: ServicesManagerDelegate, servicesManagerDosingDelegate: ServicesManagerDosingDelegate ) { @@ -52,6 +55,7 @@ class ServicesManager { self.analyticsServicesManager = analyticsServicesManager self.loggingServicesManager = loggingServicesManager self.remoteDataServicesManager = remoteDataServicesManager + self.settingsManager = settingsManager self.servicesManagerDelegate = servicesManagerDelegate self.servicesManagerDosingDelegate = servicesManagerDosingDelegate restoreState() @@ -320,6 +324,19 @@ extension ServicesManager: ServiceDelegate { func deliverRemoteBolus(amountInUnits: Double) async throws { do { + + guard amountInUnits > 0 else { + throw BolusActionError.invalidBolus + } + + guard let maxBolusAmount = settingsManager.loopSettings.maximumBolus else { + throw BolusActionError.missingMaxBolus + } + + guard amountInUnits <= maxBolusAmount else { + throw BolusActionError.exceedsMaxBolus + } + try await servicesManagerDosingDelegate?.deliverBolus(amountInUnits: amountInUnits) await NotificationManager.sendRemoteBolusNotification(amount: amountInUnits) await remoteDataServicesManager.triggerUpload(for: .dose) @@ -329,6 +346,24 @@ extension ServicesManager: ServiceDelegate { throw error } } + + enum BolusActionError: LocalizedError { + + case invalidBolus + case missingMaxBolus + case exceedsMaxBolus + + var errorDescription: String? { + switch self { + case .invalidBolus: + return NSLocalizedString("Invalid Bolus Amount", comment: "Bolus error description: invalid bolus amount.") + case .missingMaxBolus: + return NSLocalizedString("Missing maximum allowed bolus in settings", comment: "Bolus error description: missing maximum bolus in settings.") + case .exceedsMaxBolus: + return NSLocalizedString("Exceeds maximum allowed bolus in settings", comment: "Bolus error description: bolus exceeds maximum bolus in settings.") + } + } + } } extension ServicesManager: AlertIssuer {