Selaa lähdekoodia

Split up TrioRemoteControl using extensions

Jonas Björkert 1 vuosi sitten
vanhempi
commit
f788c4444a

+ 24 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -442,6 +442,12 @@
 		DD1745552C55CA6C00211FAC /* UnitsLimitsSettingsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1745542C55CA6C00211FAC /* UnitsLimitsSettingsRootView.swift */; };
 		DD1DB7CC2BECCA1F0048B367 /* BuildDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1DB7CB2BECCA1F0048B367 /* BuildDetails.swift */; };
 		DD21FCB52C6952AD00AF2C25 /* DecimalPickerSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD21FCB42C6952AD00AF2C25 /* DecimalPickerSettings.swift */; };
+		DD32CF982CC82463003686D6 /* TrioRemoteControl+Bolus.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD32CF972CC82460003686D6 /* TrioRemoteControl+Bolus.swift */; };
+		DD32CF9A2CC8247B003686D6 /* TrioRemoteControl+Meal.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD32CF992CC8246F003686D6 /* TrioRemoteControl+Meal.swift */; };
+		DD32CF9C2CC82499003686D6 /* TrioRemoteControl+TempTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD32CF9B2CC82495003686D6 /* TrioRemoteControl+TempTarget.swift */; };
+		DD32CF9E2CC824C5003686D6 /* TrioRemoteControl+Override.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD32CF9D2CC824C2003686D6 /* TrioRemoteControl+Override.swift */; };
+		DD32CFA02CC824D6003686D6 /* TrioRemoteControl+APNS.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD32CF9F2CC824D3003686D6 /* TrioRemoteControl+APNS.swift */; };
+		DD32CFA22CC824E2003686D6 /* TrioRemoteControl+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD32CFA12CC824E1003686D6 /* TrioRemoteControl+Helpers.swift */; };
 		DD68889D2C386E17006E3C44 /* NightscoutExercise.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD68889C2C386E17006E3C44 /* NightscoutExercise.swift */; };
 		DD6B7CB22C7B6F0800B75029 /* Rounding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CB12C7B6F0800B75029 /* Rounding.swift */; };
 		DD6B7CB42C7B71F700B75029 /* ForecastDisplayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */; };
@@ -1115,6 +1121,12 @@
 		DD1745542C55CA6C00211FAC /* UnitsLimitsSettingsRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitsLimitsSettingsRootView.swift; sourceTree = "<group>"; };
 		DD1DB7CB2BECCA1F0048B367 /* BuildDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildDetails.swift; sourceTree = "<group>"; };
 		DD21FCB42C6952AD00AF2C25 /* DecimalPickerSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecimalPickerSettings.swift; sourceTree = "<group>"; };
+		DD32CF972CC82460003686D6 /* TrioRemoteControl+Bolus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TrioRemoteControl+Bolus.swift"; sourceTree = "<group>"; };
+		DD32CF992CC8246F003686D6 /* TrioRemoteControl+Meal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TrioRemoteControl+Meal.swift"; sourceTree = "<group>"; };
+		DD32CF9B2CC82495003686D6 /* TrioRemoteControl+TempTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TrioRemoteControl+TempTarget.swift"; sourceTree = "<group>"; };
+		DD32CF9D2CC824C2003686D6 /* TrioRemoteControl+Override.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TrioRemoteControl+Override.swift"; sourceTree = "<group>"; };
+		DD32CF9F2CC824D3003686D6 /* TrioRemoteControl+APNS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TrioRemoteControl+APNS.swift"; sourceTree = "<group>"; };
+		DD32CFA12CC824E1003686D6 /* TrioRemoteControl+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TrioRemoteControl+Helpers.swift"; sourceTree = "<group>"; };
 		DD68889C2C386E17006E3C44 /* NightscoutExercise.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutExercise.swift; sourceTree = "<group>"; };
 		DD6B7CB12C7B6F0800B75029 /* Rounding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Rounding.swift; sourceTree = "<group>"; };
 		DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastDisplayType.swift; sourceTree = "<group>"; };
@@ -2723,6 +2735,12 @@
 		DD9ECB662CA99EFE00AA7C45 /* RemoteControl */ = {
 			isa = PBXGroup;
 			children = (
+				DD32CFA12CC824E1003686D6 /* TrioRemoteControl+Helpers.swift */,
+				DD32CF9F2CC824D3003686D6 /* TrioRemoteControl+APNS.swift */,
+				DD32CF9D2CC824C2003686D6 /* TrioRemoteControl+Override.swift */,
+				DD32CF9B2CC82495003686D6 /* TrioRemoteControl+TempTarget.swift */,
+				DD32CF992CC8246F003686D6 /* TrioRemoteControl+Meal.swift */,
+				DD32CF972CC82460003686D6 /* TrioRemoteControl+Bolus.swift */,
 				DD9ECB672CA99F4500AA7C45 /* TrioRemoteControl.swift */,
 			);
 			path = RemoteControl;
@@ -3338,6 +3356,7 @@
 				389A572026079BAA00BC102F /* Interpolation.swift in Sources */,
 				DD9ECB702CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift in Sources */,
 				19A910382A24EF3200C8951B /* ChartsView.swift in Sources */,
+				DD32CF9A2CC8247B003686D6 /* TrioRemoteControl+Meal.swift in Sources */,
 				BDF34F832C10C5B600D51995 /* DataManager.swift in Sources */,
 				38B4F3C625E5017E00E76A18 /* NotificationCenter.swift in Sources */,
 				19D466A729AA2C22004D5F33 /* MealSettingsStateModel.swift in Sources */,
@@ -3367,6 +3386,7 @@
 				388358C825EEF6D200E024B2 /* BasalProfileEntry.swift in Sources */,
 				3811DE0B25C9D32F00A708ED /* BaseView.swift in Sources */,
 				3811DE3225C9D49500A708ED /* HomeDataFlow.swift in Sources */,
+				DD32CFA22CC824E2003686D6 /* TrioRemoteControl+Helpers.swift in Sources */,
 				CE1856F52ADC4858007E39C7 /* AddCarbPresetIntent.swift in Sources */,
 				38569347270B5DFB0002C50D /* CGMType.swift in Sources */,
 				3821ED4C25DD18BA00BC42AD /* Constants.swift in Sources */,
@@ -3415,6 +3435,7 @@
 				CE7950242997D81700FA576E /* CGMSettingsView.swift in Sources */,
 				58237D9E2BCF0A6B00A47A79 /* PopupView.swift in Sources */,
 				38D0B3D925EC07C400CB6E88 /* CarbsEntry.swift in Sources */,
+				DD32CF9C2CC82499003686D6 /* TrioRemoteControl+TempTarget.swift in Sources */,
 				38A9260525F012D8009E3739 /* CarbRatios.swift in Sources */,
 				38FCF3D625E8FDF40078B0D1 /* MD5.swift in Sources */,
 				DDD163142C4C68D300CD525A /* OverrideProvider.swift in Sources */,
@@ -3475,6 +3496,7 @@
 				72F1BD388F42FCA6C52E4500 /* ConfigEditorProvider.swift in Sources */,
 				E39E418C56A5A46B61D960EE /* ConfigEditorStateModel.swift in Sources */,
 				45717281F743594AA9D87191 /* ConfigEditorRootView.swift in Sources */,
+				DD32CFA02CC824D6003686D6 /* TrioRemoteControl+APNS.swift in Sources */,
 				CE7CA3532A064973004BE681 /* tempPresetIntent.swift in Sources */,
 				D6DEC113821A7F1056C4AA1E /* NightscoutConfigDataFlow.swift in Sources */,
 				DD6B7CBB2C7FBBFA00B75029 /* ReviewInsulinActionView.swift in Sources */,
@@ -3554,6 +3576,7 @@
 				38192E0D261BAF980094D973 /* ConvenienceExtensions.swift in Sources */,
 				88AB39B23C9552BD6E0C9461 /* ISFEditorRootView.swift in Sources */,
 				BD6EB2D62C7D049B0086BBB6 /* LiveActivityWidgetConfiguration.swift in Sources */,
+				DD32CF982CC82463003686D6 /* TrioRemoteControl+Bolus.swift in Sources */,
 				F816825E28DB441200054060 /* HeartBeatManager.swift in Sources */,
 				58F107742BD1A4D000B1A680 /* Determination+helper.swift in Sources */,
 				38FEF413273B317A00574A46 /* HKUnit.swift in Sources */,
@@ -3597,6 +3620,7 @@
 				38A00B1F25FC00F7006BC0B0 /* Autotune.swift in Sources */,
 				38AAF85525FFF846004AF583 /* CurrentGlucoseView.swift in Sources */,
 				041D1E995A6AE92E9289DC49 /* BolusDataFlow.swift in Sources */,
+				DD32CF9E2CC824C5003686D6 /* TrioRemoteControl+Override.swift in Sources */,
 				23888883D4EA091C88480FF2 /* BolusProvider.swift in Sources */,
 				38E98A2D25F52DC400C0CED0 /* NSLocking+Extensions.swift in Sources */,
 				BDBAACFA2C2D439700370AAE /* OverrideData.swift in Sources */,

+ 36 - 0
FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+APNS.swift

@@ -0,0 +1,36 @@
+import Foundation
+
+extension TrioRemoteControl {
+    internal func handleAPNSChanges(deviceToken: String?) async {
+        let previousDeviceToken = UserDefaults.standard.string(forKey: "deviceToken")
+        let previousIsAPNSProduction = UserDefaults.standard.bool(forKey: "isAPNSProduction")
+
+        let isAPNSProduction = isRunningInAPNSProductionEnvironment()
+        var shouldUploadProfiles = false
+
+        if let token = deviceToken, token != previousDeviceToken {
+            UserDefaults.standard.set(token, forKey: "deviceToken")
+            debug(.remoteControl, "Device token updated: \(token)")
+            shouldUploadProfiles = true
+        }
+
+        if previousIsAPNSProduction != isAPNSProduction {
+            UserDefaults.standard.set(isAPNSProduction, forKey: "isAPNSProduction")
+            debug(.remoteControl, "APNS environment changed to: \(isAPNSProduction ? "Production" : "Sandbox")")
+            shouldUploadProfiles = true
+        }
+
+        if shouldUploadProfiles {
+            await nightscoutManager.uploadProfiles()
+        } else {
+            debug(.remoteControl, "No changes detected in device token or APNS environment.")
+        }
+    }
+
+    private func isRunningInAPNSProductionEnvironment() -> Bool {
+        if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL {
+            return appStoreReceiptURL.lastPathComponent != "sandboxReceipt"
+        }
+        return false
+    }
+}

+ 108 - 0
FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+Bolus.swift

@@ -0,0 +1,108 @@
+import Foundation
+
+extension TrioRemoteControl {
+    internal func handleBolusCommand(_ pushMessage: PushMessage) async {
+        guard let bolusAmount = pushMessage.bolusAmount else {
+            await logError("Command rejected: bolus amount is missing or invalid.", pushMessage: pushMessage)
+            return
+        }
+
+        let maxBolus = await FreeAPSApp.resolver.resolve(SettingsManager.self)?.pumpSettings.maxBolus ?? Decimal(0)
+
+        if bolusAmount > maxBolus {
+            await logError(
+                "Command rejected: bolus amount (\(bolusAmount) units) exceeds the maximum allowed (\(maxBolus) units).",
+                pushMessage: pushMessage
+            )
+            return
+        }
+
+        let maxIOB = settings.preferences.maxIOB
+        let currentIOB = await fetchCurrentIOB()
+        if (currentIOB + bolusAmount) > maxIOB {
+            await logError(
+                "Command rejected: bolus amount (\(bolusAmount) units) would exceed max IOB (\(maxIOB) units). Current IOB: \(currentIOB) units.",
+                pushMessage: pushMessage
+            )
+            return
+        }
+
+        let totalRecentBolusAmount = await fetchTotalRecentBolusAmount(since: Date(timeIntervalSince1970: pushMessage.timestamp))
+
+        if totalRecentBolusAmount >= bolusAmount * 0.2 {
+            await logError(
+                "Command rejected: boluses totaling more than 20% of the requested amount have been delivered since the command was sent.",
+                pushMessage: pushMessage
+            )
+            return
+        }
+
+        debug(.remoteControl, "Enacting bolus command with amount: \(bolusAmount) units.")
+
+        guard let apsManager = await FreeAPSApp.resolver.resolve(APSManager.self) else {
+            await logError(
+                "Error: unable to process bolus command because the APS Manager is not available.",
+                pushMessage: pushMessage
+            )
+            return
+        }
+
+        await apsManager.enactBolus(amount: Double(truncating: bolusAmount as NSNumber), isSMB: false)
+
+        debug(
+            .remoteControl,
+            "Remote command processed successfully. \(pushMessage.humanReadableDescription())"
+        )
+    }
+
+    private func fetchCurrentIOB() async -> Decimal {
+        let predicate = NSPredicate.predicateFor30MinAgoForDetermination
+
+        let determinations = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: OrefDetermination.self,
+            onContext: pumpHistoryFetchContext,
+            predicate: predicate,
+            key: "timestamp",
+            ascending: false,
+            fetchLimit: 1,
+            propertiesToFetch: ["iob"]
+        )
+
+        guard let fetchedResults = determinations as? [[String: Any]],
+              let firstResult = fetchedResults.first,
+              let iob = firstResult["iob"] as? Decimal
+        else {
+            await logError("Failed to fetch current IOB.")
+            return Decimal(0)
+        }
+
+        return iob
+    }
+
+    private func fetchTotalRecentBolusAmount(since date: Date) async -> Decimal {
+        let predicate = NSPredicate(
+            format: "type == %@ AND timestamp > %@",
+            PumpEventStored.EventType.bolus.rawValue,
+            date as NSDate
+        )
+
+        let results: Any = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: PumpEventStored.self,
+            onContext: pumpHistoryFetchContext,
+            predicate: predicate,
+            key: "timestamp",
+            ascending: true,
+            fetchLimit: nil,
+            propertiesToFetch: ["bolus.amount"]
+        )
+
+        guard let bolusDictionaries = results as? [[String: Any]] else {
+            await logError("Failed to cast fetched bolus events. Fetched entities type: \(type(of: results))")
+            return 0
+        }
+
+        let totalAmount = bolusDictionaries.compactMap { ($0["bolus.amount"] as? NSNumber)?.decimalValue }.reduce(0, +)
+
+        return totalAmount
+    }
+}

+ 12 - 0
FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+Helpers.swift

@@ -0,0 +1,12 @@
+import Foundation
+
+extension TrioRemoteControl {
+    func logError(_ errorMessage: String, pushMessage: PushMessage? = nil) async {
+        var note = errorMessage
+        if let pushMessage = pushMessage {
+            note += " Details: \(pushMessage.humanReadableDescription())"
+        }
+        debug(.remoteControl, note)
+        await nightscoutManager.uploadNoteTreatment(note: note)
+    }
+}

+ 82 - 0
FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+Meal.swift

@@ -0,0 +1,82 @@
+import Foundation
+
+extension TrioRemoteControl {
+    func handleMealCommand(_ pushMessage: PushMessage) async {
+        guard pushMessage.carbs != nil || pushMessage.fat != nil || pushMessage.protein != nil else {
+            await logError("Command rejected: meal data is incomplete or invalid.", pushMessage: pushMessage)
+            return
+        }
+
+        let carbsDecimal = pushMessage.carbs != nil ? Decimal(pushMessage.carbs!) : nil
+        let fatDecimal = pushMessage.fat != nil ? Decimal(pushMessage.fat!) : nil
+        let proteinDecimal = pushMessage.protein != nil ? Decimal(pushMessage.protein!) : nil
+
+        let settings = await FreeAPSApp.resolver.resolve(SettingsManager.self)?.settings
+        let maxCarbs = settings?.maxCarbs ?? Decimal(0)
+        let maxFat = settings?.maxFat ?? Decimal(0)
+        let maxProtein = settings?.maxProtein ?? Decimal(0)
+
+        if let carbs = carbsDecimal, carbs > maxCarbs {
+            await logError(
+                "Command rejected: carbs amount (\(carbs)g) exceeds the maximum allowed (\(maxCarbs)g).",
+                pushMessage: pushMessage
+            )
+            return
+        }
+
+        if let fat = fatDecimal, fat > maxFat {
+            await logError(
+                "Command rejected: fat amount (\(fat)g) exceeds the maximum allowed (\(maxFat)g).",
+                pushMessage: pushMessage
+            )
+            return
+        }
+
+        if let protein = proteinDecimal, protein > maxProtein {
+            await logError(
+                "Command rejected: protein amount (\(protein)g) exceeds the maximum allowed (\(maxProtein)g).",
+                pushMessage: pushMessage
+            )
+            return
+        }
+
+        let pushMessageDate = Date(timeIntervalSince1970: pushMessage.timestamp)
+        let recentCarbEntries = carbsStorage.recent()
+        let carbsAfterPushMessage = recentCarbEntries.filter { $0.createdAt > pushMessageDate }
+
+        if !carbsAfterPushMessage.isEmpty {
+            await logError(
+                "Command rejected: newer carb entries have been logged since the command was sent.",
+                pushMessage: pushMessage
+            )
+            return
+        }
+
+        let actualDate: Date?
+        if let scheduledTime = pushMessage.scheduledTime {
+            actualDate = Date(timeIntervalSince1970: scheduledTime)
+        } else {
+            actualDate = nil
+        }
+
+        let mealEntry = CarbsEntry(
+            id: UUID().uuidString,
+            createdAt: Date(),
+            actualDate: actualDate,
+            carbs: carbsDecimal ?? 0,
+            fat: fatDecimal,
+            protein: proteinDecimal,
+            note: "Remote meal command",
+            enteredBy: CarbsEntry.manual,
+            isFPU: false,
+            fpuID: nil
+        )
+
+        await carbsStorage.storeCarbs([mealEntry], areFetchedFromRemote: false)
+
+        debug(
+            .remoteControl,
+            "Remote command processed successfully. \(pushMessage.humanReadableDescription())"
+        )
+    }
+}

+ 100 - 0
FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+Override.swift

@@ -0,0 +1,100 @@
+import Foundation
+
+extension TrioRemoteControl {
+    @MainActor internal func handleCancelOverrideCommand(_ pushMessage: PushMessage) async {
+        await disableAllActiveOverrides()
+
+        debug(
+            .remoteControl,
+            "Remote command processed successfully. \(pushMessage.humanReadableDescription())"
+        )
+    }
+
+    @MainActor internal func handleStartOverrideCommand(_ pushMessage: PushMessage) async {
+        guard let overrideName = pushMessage.overrideName, !overrideName.isEmpty else {
+            await logError("Command rejected: override name is missing.", pushMessage: pushMessage)
+            return
+        }
+
+        let presetIDs = await overrideStorage.fetchForOverridePresets()
+
+        let presets = presetIDs.compactMap { id in
+            try? viewContext.existingObject(with: id) as? OverrideStored
+        }
+
+        if let preset = presets.first(where: { $0.name == overrideName }) {
+            await enactOverridePreset(preset: preset, pushMessage: pushMessage)
+        } else {
+            await logError("Command rejected: override preset '\(overrideName)' not found.", pushMessage: pushMessage)
+        }
+    }
+
+    @MainActor private func enactOverridePreset(preset: OverrideStored, pushMessage: PushMessage) async {
+        await disableAllActiveOverrides()
+
+        preset.enabled = true
+        preset.date = Date()
+        preset.isUploadedToNS = false
+
+        do {
+            if viewContext.hasChanges {
+                try viewContext.save()
+
+                Foundation.NotificationCenter.default.post(name: .willUpdateOverrideConfiguration, object: nil)
+                await awaitNotification(.didUpdateOverrideConfiguration)
+
+                debug(
+                    .remoteControl,
+                    "Remote command processed successfully. \(pushMessage.humanReadableDescription())"
+                )
+            }
+        } catch {
+            debug(.remoteControl, "Failed to enact override preset: \(error.localizedDescription)")
+        }
+    }
+
+    @MainActor private func disableAllActiveOverrides() async {
+        let ids = await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 0) // 0 = no fetch limit
+
+        let didPostNotification = await viewContext.perform { () -> Bool in
+            do {
+                let results = try ids.compactMap { id in
+                    try self.viewContext.existingObject(with: id) as? OverrideStored
+                }
+
+                guard !results.isEmpty else { return false }
+
+                for canceledOverride in results where canceledOverride.enabled {
+                    let newOverrideRunStored = OverrideRunStored(context: self.viewContext)
+                    newOverrideRunStored.id = UUID()
+                    newOverrideRunStored.name = canceledOverride.name
+                    newOverrideRunStored.startDate = canceledOverride.date ?? .distantPast
+                    newOverrideRunStored.endDate = Date()
+                    newOverrideRunStored
+                        .target = NSDecimalNumber(decimal: self.overrideStorage.calculateTarget(override: canceledOverride))
+                    newOverrideRunStored.override = canceledOverride
+                    newOverrideRunStored.isUploadedToNS = false
+
+                    canceledOverride.enabled = false
+                }
+
+                if self.viewContext.hasChanges {
+                    try self.viewContext.save()
+                    Foundation.NotificationCenter.default.post(name: .willUpdateOverrideConfiguration, object: nil)
+                    return true
+                } else {
+                    return false
+                }
+            } catch {
+                debugPrint(
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to disable active Overrides with error: \(error.localizedDescription)"
+                )
+                return false
+            }
+        }
+
+        if didPostNotification {
+            await awaitNotification(.didUpdateOverrideConfiguration)
+        }
+    }
+}

+ 49 - 0
FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+TempTarget.swift

@@ -0,0 +1,49 @@
+import Foundation
+
+extension TrioRemoteControl {
+    func handleTempTargetCommand(_ pushMessage: PushMessage) async {
+        guard let targetValue = pushMessage.target,
+              let durationValue = pushMessage.duration
+        else {
+            await logError("Command rejected: temp target data is incomplete or invalid.", pushMessage: pushMessage)
+            return
+        }
+
+        let durationInMinutes = Int(durationValue)
+        let pushMessageDate = Date(timeIntervalSince1970: pushMessage.timestamp)
+
+        let tempTarget = TempTarget(
+            name: TempTarget.custom,
+            createdAt: pushMessageDate,
+            targetTop: Decimal(targetValue),
+            targetBottom: Decimal(targetValue),
+            duration: Decimal(durationInMinutes),
+            enteredBy: TempTarget.manual,
+            reason: TempTarget.custom
+        )
+
+        tempTargetsStorage.storeTempTargets([tempTarget])
+
+        debug(
+            .remoteControl,
+            "Remote command processed successfully. \(pushMessage.humanReadableDescription())"
+        )
+    }
+
+    func cancelTempTarget(_ pushMessage: PushMessage) async {
+        debug(.remoteControl, "Cancelling temp target.")
+
+        guard tempTargetsStorage.current() != nil else {
+            await logError("Command rejected: no active temp target to cancel.")
+            return
+        }
+
+        let cancelEntry = TempTarget.cancel(at: Date())
+        tempTargetsStorage.storeTempTargets([cancelEntry])
+
+        debug(
+            .remoteControl,
+            "Remote command processed successfully. \(pushMessage.humanReadableDescription())"
+        )
+    }
+}

+ 7 - 376
FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl.swift

@@ -5,16 +5,16 @@ import Swinject
 class TrioRemoteControl: Injectable {
     static let shared = TrioRemoteControl()
 
-    @Injected() private var tempTargetsStorage: TempTargetsStorage!
-    @Injected() private var carbsStorage: CarbsStorage!
-    @Injected() private var nightscoutManager: NightscoutManager!
-    @Injected() private var overrideStorage: OverrideStorage!
-    @Injected() private var settings: SettingsManager!
+    @Injected() internal var tempTargetsStorage: TempTargetsStorage!
+    @Injected() internal var carbsStorage: CarbsStorage!
+    @Injected() internal var nightscoutManager: NightscoutManager!
+    @Injected() internal var overrideStorage: OverrideStorage!
+    @Injected() internal var settings: SettingsManager!
 
     private let timeWindow: TimeInterval = 600 // Defines how old messages that are accepted, 10 minutes
 
-    private let pumpHistoryFetchContext: NSManagedObjectContext
-    private let viewContext: NSManagedObjectContext
+    internal let pumpHistoryFetchContext: NSManagedObjectContext
+    internal let viewContext: NSManagedObjectContext
 
     private init() {
         pumpHistoryFetchContext = CoreDataStack.shared.newTaskContext()
@@ -22,15 +22,6 @@ class TrioRemoteControl: Injectable {
         injectServices(FreeAPSApp.resolver)
     }
 
-    private func logError(_ errorMessage: String, pushMessage: PushMessage? = nil) async {
-        var note = errorMessage
-        if let pushMessage = pushMessage {
-            note += " Details: \(pushMessage.humanReadableDescription())"
-        }
-        debug(.remoteControl, note)
-        await nightscoutManager.uploadNoteTreatment(note: note)
-    }
-
     func handleRemoteNotification(pushMessage: PushMessage) async {
         let isTrioRemoteControlEnabled = UserDefaults.standard.bool(forKey: "isTrioRemoteControlEnabled")
         guard isTrioRemoteControlEnabled else {
@@ -89,366 +80,6 @@ class TrioRemoteControl: Injectable {
             await handleCancelOverrideCommand(pushMessage)
         }
     }
-
-    private func handleMealCommand(_ pushMessage: PushMessage) async {
-        guard pushMessage.carbs != nil || pushMessage.fat != nil || pushMessage.protein != nil else {
-            await logError("Command rejected: meal data is incomplete or invalid.", pushMessage: pushMessage)
-            return
-        }
-
-        let carbsDecimal = pushMessage.carbs != nil ? Decimal(pushMessage.carbs!) : nil
-        let fatDecimal = pushMessage.fat != nil ? Decimal(pushMessage.fat!) : nil
-        let proteinDecimal = pushMessage.protein != nil ? Decimal(pushMessage.protein!) : nil
-
-        let settings = await FreeAPSApp.resolver.resolve(SettingsManager.self)?.settings
-        let maxCarbs = settings?.maxCarbs ?? Decimal(0)
-        let maxFat = settings?.maxFat ?? Decimal(0)
-        let maxProtein = settings?.maxProtein ?? Decimal(0)
-
-        if let carbs = carbsDecimal, carbs > maxCarbs {
-            await logError(
-                "Command rejected: carbs amount (\(carbs)g) exceeds the maximum allowed (\(maxCarbs)g).",
-                pushMessage: pushMessage
-            )
-            return
-        }
-
-        if let fat = fatDecimal, fat > maxFat {
-            await logError(
-                "Command rejected: fat amount (\(fat)g) exceeds the maximum allowed (\(maxFat)g).",
-                pushMessage: pushMessage
-            )
-            return
-        }
-
-        if let protein = proteinDecimal, protein > maxProtein {
-            await logError(
-                "Command rejected: protein amount (\(protein)g) exceeds the maximum allowed (\(maxProtein)g).",
-                pushMessage: pushMessage
-            )
-            return
-        }
-
-        let pushMessageDate = Date(timeIntervalSince1970: pushMessage.timestamp)
-        let recentCarbEntries = carbsStorage.recent()
-        let carbsAfterPushMessage = recentCarbEntries.filter { $0.createdAt > pushMessageDate }
-
-        if !carbsAfterPushMessage.isEmpty {
-            await logError(
-                "Command rejected: newer carb entries have been logged since the command was sent.",
-                pushMessage: pushMessage
-            )
-            return
-        }
-
-        let actualDate: Date?
-        if let scheduledTime = pushMessage.scheduledTime {
-            actualDate = Date(timeIntervalSince1970: scheduledTime)
-        } else {
-            actualDate = nil
-        }
-
-        let mealEntry = CarbsEntry(
-            id: UUID().uuidString,
-            createdAt: Date(),
-            actualDate: actualDate,
-            carbs: carbsDecimal ?? 0,
-            fat: fatDecimal,
-            protein: proteinDecimal,
-            note: "Remote meal command",
-            enteredBy: CarbsEntry.manual,
-            isFPU: false,
-            fpuID: nil
-        )
-
-        await carbsStorage.storeCarbs([mealEntry], areFetchedFromRemote: false)
-
-        debug(
-            .remoteControl,
-            "Remote command processed successfully. \(pushMessage.humanReadableDescription())"
-        )
-    }
-
-    private func handleBolusCommand(_ pushMessage: PushMessage) async {
-        guard let bolusAmount = pushMessage.bolusAmount else {
-            await logError("Command rejected: bolus amount is missing or invalid.", pushMessage: pushMessage)
-            return
-        }
-
-        let maxBolus = await FreeAPSApp.resolver.resolve(SettingsManager.self)?.pumpSettings.maxBolus ?? Decimal(0)
-
-        if bolusAmount > maxBolus {
-            await logError(
-                "Command rejected: bolus amount (\(bolusAmount) units) exceeds the maximum allowed (\(maxBolus) units).",
-                pushMessage: pushMessage
-            )
-            return
-        }
-
-        let maxIOB = settings.preferences.maxIOB
-        let currentIOB = await fetchCurrentIOB()
-        if (currentIOB + bolusAmount) > maxIOB {
-            await logError(
-                "Command rejected: bolus amount (\(bolusAmount) units) would exceed max IOB (\(maxIOB) units). Current IOB: \(currentIOB) units.",
-                pushMessage: pushMessage
-            )
-            return
-        }
-
-        let totalRecentBolusAmount = await fetchTotalRecentBolusAmount(since: Date(timeIntervalSince1970: pushMessage.timestamp))
-
-        if totalRecentBolusAmount >= bolusAmount * 0.2 {
-            await logError(
-                "Command rejected: boluses totaling more than 20% of the requested amount have been delivered since the command was sent.",
-                pushMessage: pushMessage
-            )
-            return
-        }
-
-        debug(.remoteControl, "Enacting bolus command with amount: \(bolusAmount) units.")
-
-        guard let apsManager = await FreeAPSApp.resolver.resolve(APSManager.self) else {
-            await logError(
-                "Error: unable to process bolus command because the APS Manager is not available.",
-                pushMessage: pushMessage
-            )
-            return
-        }
-
-        await apsManager.enactBolus(amount: Double(truncating: bolusAmount as NSNumber), isSMB: false)
-
-        debug(
-            .remoteControl,
-            "Remote command processed successfully. \(pushMessage.humanReadableDescription())"
-        )
-    }
-
-    private func fetchCurrentIOB() async -> Decimal {
-        let predicate = NSPredicate.predicateFor30MinAgoForDetermination
-
-        let determinations = await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: OrefDetermination.self,
-            onContext: pumpHistoryFetchContext,
-            predicate: predicate,
-            key: "timestamp",
-            ascending: false,
-            fetchLimit: 1,
-            propertiesToFetch: ["iob"]
-        )
-
-        guard let fetchedResults = determinations as? [[String: Any]],
-              let firstResult = fetchedResults.first,
-              let iob = firstResult["iob"] as? Decimal
-        else {
-            await logError("Failed to fetch current IOB.")
-            return Decimal(0)
-        }
-
-        return iob
-    }
-
-    private func fetchTotalRecentBolusAmount(since date: Date) async -> Decimal {
-        let predicate = NSPredicate(
-            format: "type == %@ AND timestamp > %@",
-            PumpEventStored.EventType.bolus.rawValue,
-            date as NSDate
-        )
-
-        let results: Any = await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: PumpEventStored.self,
-            onContext: pumpHistoryFetchContext,
-            predicate: predicate,
-            key: "timestamp",
-            ascending: true,
-            fetchLimit: nil,
-            propertiesToFetch: ["bolus.amount"]
-        )
-
-        guard let bolusDictionaries = results as? [[String: Any]] else {
-            await logError("Failed to cast fetched bolus events. Fetched entities type: \(type(of: results))")
-            return 0
-        }
-
-        let totalAmount = bolusDictionaries.compactMap { ($0["bolus.amount"] as? NSNumber)?.decimalValue }.reduce(0, +)
-
-        return totalAmount
-    }
-
-    private func handleTempTargetCommand(_ pushMessage: PushMessage) async {
-        guard let targetValue = pushMessage.target,
-              let durationValue = pushMessage.duration
-        else {
-            await logError("Command rejected: temp target data is incomplete or invalid.", pushMessage: pushMessage)
-            return
-        }
-
-        let durationInMinutes = Int(durationValue)
-        let pushMessageDate = Date(timeIntervalSince1970: pushMessage.timestamp)
-
-        let tempTarget = TempTarget(
-            name: TempTarget.custom,
-            createdAt: pushMessageDate,
-            targetTop: Decimal(targetValue),
-            targetBottom: Decimal(targetValue),
-            duration: Decimal(durationInMinutes),
-            enteredBy: TempTarget.manual,
-            reason: TempTarget.custom
-        )
-
-        tempTargetsStorage.storeTempTargets([tempTarget])
-
-        debug(
-            .remoteControl,
-            "Remote command processed successfully. \(pushMessage.humanReadableDescription())"
-        )
-    }
-
-    func cancelTempTarget(_ pushMessage: PushMessage) async {
-        debug(.remoteControl, "Cancelling temp target.")
-
-        guard tempTargetsStorage.current() != nil else {
-            await logError("Command rejected: no active temp target to cancel.")
-            return
-        }
-
-        let cancelEntry = TempTarget.cancel(at: Date())
-        tempTargetsStorage.storeTempTargets([cancelEntry])
-
-        debug(
-            .remoteControl,
-            "Remote command processed successfully. \(pushMessage.humanReadableDescription())"
-        )
-    }
-
-    @MainActor private func handleCancelOverrideCommand(_ pushMessage: PushMessage) async {
-        await disableAllActiveOverrides()
-
-        debug(
-            .remoteControl,
-            "Remote command processed successfully. \(pushMessage.humanReadableDescription())"
-        )
-    }
-
-    @MainActor private func handleStartOverrideCommand(_ pushMessage: PushMessage) async {
-        guard let overrideName = pushMessage.overrideName, !overrideName.isEmpty else {
-            await logError("Command rejected: override name is missing.", pushMessage: pushMessage)
-            return
-        }
-
-        let presetIDs = await overrideStorage.fetchForOverridePresets()
-
-        let presets = presetIDs.compactMap { id in
-            try? viewContext.existingObject(with: id) as? OverrideStored
-        }
-
-        if let preset = presets.first(where: { $0.name == overrideName }) {
-            await enactOverridePreset(preset: preset, pushMessage: pushMessage)
-        } else {
-            await logError("Command rejected: override preset '\(overrideName)' not found.", pushMessage: pushMessage)
-        }
-    }
-
-    @MainActor private func enactOverridePreset(preset: OverrideStored, pushMessage: PushMessage) async {
-        await disableAllActiveOverrides()
-
-        preset.enabled = true
-        preset.date = Date()
-        preset.isUploadedToNS = false
-
-        do {
-            if viewContext.hasChanges {
-                try viewContext.save()
-
-                Foundation.NotificationCenter.default.post(name: .willUpdateOverrideConfiguration, object: nil)
-                await awaitNotification(.didUpdateOverrideConfiguration)
-
-                debug(
-                    .remoteControl,
-                    "Remote command processed successfully. \(pushMessage.humanReadableDescription())"
-                )
-            }
-        } catch {
-            debug(.remoteControl, "Failed to enact override preset: \(error.localizedDescription)")
-        }
-    }
-
-    @MainActor func disableAllActiveOverrides() async {
-        let ids = await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 0) // 0 = no fetch limit
-
-        let didPostNotification = await viewContext.perform { () -> Bool in
-            do {
-                let results = try ids.compactMap { id in
-                    try self.viewContext.existingObject(with: id) as? OverrideStored
-                }
-
-                guard !results.isEmpty else { return false }
-
-                for canceledOverride in results where canceledOverride.enabled {
-                    let newOverrideRunStored = OverrideRunStored(context: self.viewContext)
-                    newOverrideRunStored.id = UUID()
-                    newOverrideRunStored.name = canceledOverride.name
-                    newOverrideRunStored.startDate = canceledOverride.date ?? .distantPast
-                    newOverrideRunStored.endDate = Date()
-                    newOverrideRunStored
-                        .target = NSDecimalNumber(decimal: self.overrideStorage.calculateTarget(override: canceledOverride))
-                    newOverrideRunStored.override = canceledOverride
-                    newOverrideRunStored.isUploadedToNS = false
-
-                    canceledOverride.enabled = false
-                }
-
-                if self.viewContext.hasChanges {
-                    try self.viewContext.save()
-                    Foundation.NotificationCenter.default.post(name: .willUpdateOverrideConfiguration, object: nil)
-                    return true
-                } else {
-                    return false
-                }
-            } catch {
-                debugPrint(
-                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to disable active Overrides with error: \(error.localizedDescription)"
-                )
-                return false
-            }
-        }
-
-        if didPostNotification {
-            await awaitNotification(.didUpdateOverrideConfiguration)
-        }
-    }
-
-    func handleAPNSChanges(deviceToken: String?) async {
-        let previousDeviceToken = UserDefaults.standard.string(forKey: "deviceToken")
-        let previousIsAPNSProduction = UserDefaults.standard.bool(forKey: "isAPNSProduction")
-
-        let isAPNSProduction = isRunningInAPNSProductionEnvironment()
-        var shouldUploadProfiles = false
-
-        if let token = deviceToken, token != previousDeviceToken {
-            UserDefaults.standard.set(token, forKey: "deviceToken")
-            debug(.remoteControl, "Device token updated: \(token)")
-            shouldUploadProfiles = true
-        }
-
-        if previousIsAPNSProduction != isAPNSProduction {
-            UserDefaults.standard.set(isAPNSProduction, forKey: "isAPNSProduction")
-            debug(.remoteControl, "APNS environment changed to: \(isAPNSProduction ? "Production" : "Sandbox")")
-            shouldUploadProfiles = true
-        }
-
-        if shouldUploadProfiles {
-            await nightscoutManager.uploadProfiles()
-        } else {
-            debug(.remoteControl, "No changes detected in device token or APNS environment.")
-        }
-    }
-
-    private func isRunningInAPNSProductionEnvironment() -> Bool {
-        if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL {
-            return appStoreReceiptURL.lastPathComponent != "sandboxReceipt"
-        }
-        return false
-    }
 }
 
 // MARK: - CommandType Enum