فهرست منبع

Merge pull request #362 from loopandlearn/loop-override-pr

Enable Setting and Canceling Loop Overrides
Marion Barker 1 سال پیش
والد
کامیت
19d0c22f86

+ 42 - 10
LoopFollow.xcodeproj/project.pbxproj

@@ -39,7 +39,7 @@
 		DD4878132C7B750D0048F05C /* TempTargetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4878122C7B750D0048F05C /* TempTargetView.swift */; };
 		DD4878152C7B75230048F05C /* MealView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4878142C7B75230048F05C /* MealView.swift */; };
 		DD4878172C7B75350048F05C /* BolusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4878162C7B75350048F05C /* BolusView.swift */; };
-		DD4878192C7C56D60048F05C /* TrioNightscoutRemoteController.swift.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4878182C7C56D60048F05C /* TrioNightscoutRemoteController.swift.swift */; };
+		DD4878192C7C56D60048F05C /* TrioNightscoutRemoteController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4878182C7C56D60048F05C /* TrioNightscoutRemoteController.swift */; };
 		DD48781C2C7DAF140048F05C /* SwiftJWT in Frameworks */ = {isa = PBXBuildFile; productRef = DD48781B2C7DAF140048F05C /* SwiftJWT */; };
 		DD48781E2C7DAF2F0048F05C /* PushNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD48781D2C7DAF2F0048F05C /* PushNotificationManager.swift */; };
 		DD4878202C7DAF890048F05C /* PushMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD48781F2C7DAF890048F05C /* PushMessage.swift */; };
@@ -104,6 +104,10 @@
 		DDD10F052C529DA200D76A8E /* ObservableValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD10F042C529DA200D76A8E /* ObservableValue.swift */; };
 		DDD10F072C529DE800D76A8E /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD10F062C529DE800D76A8E /* Observable.swift */; };
 		DDD10F0B2C54192A00D76A8E /* TemporaryTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD10F0A2C54192A00D76A8E /* TemporaryTarget.swift */; };
+		DDDF6F432D479A9900884336 /* LoopNightscoutRemoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDF6F422D479A9800884336 /* LoopNightscoutRemoteView.swift */; };
+		DDDF6F452D479AB100884336 /* LoopOverrideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDF6F442D479AB000884336 /* LoopOverrideView.swift */; };
+		DDDF6F472D479AD200884336 /* LoopOverrideViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDF6F462D479AD100884336 /* LoopOverrideViewModel.swift */; };
+		DDDF6F492D479AF000884336 /* NoRemoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDF6F482D479AEF00884336 /* NoRemoteView.swift */; };
 		DDE69ED22C7256260013EAEC /* RemoteType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE69ED12C7256260013EAEC /* RemoteType.swift */; };
 		DDEF503A2D31615000999A5D /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDEF50392D31614200999A5D /* LogManager.swift */; };
 		DDEF503C2D31BE2D00999A5D /* TaskScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDEF503B2D31BE2A00999A5D /* TaskScheduler.swift */; };
@@ -310,7 +314,7 @@
 		DD4878122C7B750D0048F05C /* TempTargetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetView.swift; sourceTree = "<group>"; };
 		DD4878142C7B75230048F05C /* MealView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealView.swift; sourceTree = "<group>"; };
 		DD4878162C7B75350048F05C /* BolusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusView.swift; sourceTree = "<group>"; };
-		DD4878182C7C56D60048F05C /* TrioNightscoutRemoteController.swift.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrioNightscoutRemoteController.swift.swift; sourceTree = "<group>"; };
+		DD4878182C7C56D60048F05C /* TrioNightscoutRemoteController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrioNightscoutRemoteController.swift; sourceTree = "<group>"; };
 		DD48781D2C7DAF2F0048F05C /* PushNotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationManager.swift; sourceTree = "<group>"; };
 		DD48781F2C7DAF890048F05C /* PushMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushMessage.swift; sourceTree = "<group>"; };
 		DD493AD42ACF2109009A6922 /* ResumePump.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResumePump.swift; sourceTree = "<group>"; };
@@ -375,6 +379,10 @@
 		DDD10F042C529DA200D76A8E /* ObservableValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableValue.swift; sourceTree = "<group>"; };
 		DDD10F062C529DE800D76A8E /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = "<group>"; };
 		DDD10F0A2C54192A00D76A8E /* TemporaryTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemporaryTarget.swift; sourceTree = "<group>"; };
+		DDDF6F422D479A9800884336 /* LoopNightscoutRemoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopNightscoutRemoteView.swift; sourceTree = "<group>"; };
+		DDDF6F442D479AB000884336 /* LoopOverrideView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopOverrideView.swift; sourceTree = "<group>"; };
+		DDDF6F462D479AD100884336 /* LoopOverrideViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopOverrideViewModel.swift; sourceTree = "<group>"; };
+		DDDF6F482D479AEF00884336 /* NoRemoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoRemoteView.swift; sourceTree = "<group>"; };
 		DDE69ED12C7256260013EAEC /* RemoteType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteType.swift; sourceTree = "<group>"; };
 		DDEF50392D31614200999A5D /* LogManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogManager.swift; sourceTree = "<group>"; };
 		DDEF503B2D31BE2A00999A5D /* TaskScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskScheduler.swift; sourceTree = "<group>"; };
@@ -598,18 +606,14 @@
 		DD0C0C6E2C4AFFB800DBADDF /* Remote */ = {
 			isa = PBXGroup;
 			children = (
+				DDDF6F4A2D479B6A00884336 /* Nightscout */,
+				DDDF6F482D479AEF00884336 /* NoRemoteView.swift */,
+				DDDF6F412D479A8E00884336 /* Loop */,
 				DD4878112C7B74F90048F05C /* TRC */,
 				DD4878062C7B2E9E0048F05C /* Settings */,
 				DDF699972C5AA2E50058A8D9 /* TempTargetPreset */,
 				DD0C0C6F2C4AFFE800DBADDF /* RemoteViewController.swift */,
-				DD0C0C712C4B000800DBADDF /* TrioNightscoutRemoteView.swift */,
-				DDD10F022C518A6500D76A8E /* TreatmentResponse.swift */,
 				DDE69ED12C7256260013EAEC /* RemoteType.swift */,
-				DD48780D2C7B74A40048F05C /* TrioRemoteControlViewModel.swift */,
-				DD48780F2C7B74BF0048F05C /* TrioRemoteControlView.swift */,
-				DD4878182C7C56D60048F05C /* TrioNightscoutRemoteController.swift.swift */,
-				DD48781D2C7DAF2F0048F05C /* PushNotificationManager.swift */,
-				DD48781F2C7DAF890048F05C /* PushMessage.swift */,
 			);
 			path = Remote;
 			sourceTree = "<group>";
@@ -664,9 +668,15 @@
 		DD4878112C7B74F90048F05C /* TRC */ = {
 			isa = PBXGroup;
 			children = (
+				DD48781F2C7DAF890048F05C /* PushMessage.swift */,
 				DD4878122C7B750D0048F05C /* TempTargetView.swift */,
+				DD48781D2C7DAF2F0048F05C /* PushNotificationManager.swift */,
 				DD4878142C7B75230048F05C /* MealView.swift */,
+				DD48780F2C7B74BF0048F05C /* TrioRemoteControlView.swift */,
+				DD48780D2C7B74A40048F05C /* TrioRemoteControlViewModel.swift */,
 				DD4878162C7B75350048F05C /* BolusView.swift */,
+				DDD10F022C518A6500D76A8E /* TreatmentResponse.swift */,
+				DD4878182C7C56D60048F05C /* TrioNightscoutRemoteController.swift */,
 				DDFD5C522CB167DA00D3FD68 /* TRCCommandType.swift */,
 				DDBE3ABC2CB5A961006B37DC /* OverrideView.swift */,
 			);
@@ -779,6 +789,24 @@
 			path = Scripts;
 			sourceTree = "<group>";
 		};
+		DDDF6F412D479A8E00884336 /* Loop */ = {
+			isa = PBXGroup;
+			children = (
+				DDDF6F462D479AD100884336 /* LoopOverrideViewModel.swift */,
+				DDDF6F442D479AB000884336 /* LoopOverrideView.swift */,
+				DDDF6F422D479A9800884336 /* LoopNightscoutRemoteView.swift */,
+			);
+			path = Loop;
+			sourceTree = "<group>";
+		};
+		DDDF6F4A2D479B6A00884336 /* Nightscout */ = {
+			isa = PBXGroup;
+			children = (
+				DD0C0C712C4B000800DBADDF /* TrioNightscoutRemoteView.swift */,
+			);
+			path = Nightscout;
+			sourceTree = "<group>";
+		};
 		DDEF503D2D32753A00999A5D /* Task */ = {
 			isa = PBXGroup;
 			children = (
@@ -1390,6 +1418,7 @@
 				DD608A0A2C23593900F91132 /* SMB.swift in Sources */,
 				DDCF979824C1489C002C9752 /* GraphSettingsViewController.swift in Sources */,
 				FC3AE7B5249E8E0E00AAE1E0 /* LoopFollow.xcdatamodeld in Sources */,
+				DDDF6F492D479AF000884336 /* NoRemoteView.swift in Sources */,
 				FCC6886F2489A53800A0279D /* AppConstants.swift in Sources */,
 				DD7E19842ACDA50C00DBD158 /* Overrides.swift in Sources */,
 				DDF2C0102BEFA991007A20E6 /* GitHubService.swift in Sources */,
@@ -1413,6 +1442,7 @@
 				DDE69ED22C7256260013EAEC /* RemoteType.swift in Sources */,
 				DDD10F032C518A6500D76A8E /* TreatmentResponse.swift in Sources */,
 				DDD10F0B2C54192A00D76A8E /* TemporaryTarget.swift in Sources */,
+				DDDF6F472D479AD200884336 /* LoopOverrideViewModel.swift in Sources */,
 				DD5334272C61668800062F9D /* InfoDisplaySettingsViewModel.swift in Sources */,
 				DD48780E2C7B74A40048F05C /* TrioRemoteControlViewModel.swift in Sources */,
 				DDEF503A2D31615000999A5D /* LogManager.swift in Sources */,
@@ -1475,6 +1505,7 @@
 				DD0C0C602C415B9D00DBADDF /* ProfileManager.swift in Sources */,
 				DDF699962C5582290058A8D9 /* TextFieldWithToolBar.swift in Sources */,
 				DD91E4DD2BDEC3F8002D9E97 /* GlucoseConversion.swift in Sources */,
+				DDDF6F452D479AB100884336 /* LoopOverrideView.swift in Sources */,
 				DD4878132C7B750D0048F05C /* TempTargetView.swift in Sources */,
 				DD0C0C682C48529400DBADDF /* Metric.swift in Sources */,
 				FCC6886D2489909D00A0279D /* AnyConvertible.swift in Sources */,
@@ -1485,8 +1516,9 @@
 				FCD49B6C24AA536E007879DC /* DebugViewController.swift in Sources */,
 				DD5334B02D1447C500CDD6EA /* BLEManager.swift in Sources */,
 				DD4878032C7B297E0048F05C /* StorageValue.swift in Sources */,
-				DD4878192C7C56D60048F05C /* TrioNightscoutRemoteController.swift.swift in Sources */,
+				DD4878192C7C56D60048F05C /* TrioNightscoutRemoteController.swift in Sources */,
 				FC1BDD2B24A22650001B652C /* Stats.swift in Sources */,
+				DDDF6F432D479A9900884336 /* LoopNightscoutRemoteView.swift in Sources */,
 				DDD10F052C529DA200D76A8E /* ObservableValue.swift in Sources */,
 				FC1BDD2D24A23204001B652C /* StatsView.swift in Sources */,
 				DD4878102C7B74BF0048F05C /* TrioRemoteControlView.swift in Sources */,

+ 5 - 0
LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift

@@ -14,6 +14,11 @@ import HealthKit
 extension MainViewController {
     func DeviceStatusLoop(formatter: ISO8601DateFormatter, lastLoopRecord: [String: AnyObject]) {
         ObservableUserDefaults.shared.device.value = "Loop"
+
+        if Storage.shared.remoteType.value == .trc {
+            Storage.shared.remoteType.value = .none
+        }
+
         if let lastLoopTime = formatter.date(from: (lastLoopRecord["timestamp"] as! String))?.timeIntervalSince1970  {
             UserDefaultsRepository.alertLastLoopTime.value = lastLoopTime
             if let failure = lastLoopRecord["failureReason"] {

+ 30 - 9
LoopFollow/Controllers/Nightscout/NSProfile.swift

@@ -22,13 +22,6 @@ struct NSProfile: Decodable {
             let time: String
             let timeAsSeconds: Double
         }
-        struct OverrideEntry: Decodable {
-            let name: String?
-            let targetRange: [Double]?
-            let duration: Int?
-            let insulinNeedsScaleFactor: Double?
-            let symbol: String?
-        }
         struct TargetEntry: Decodable {
             let value: Double
             let time: String
@@ -38,7 +31,6 @@ struct NSProfile: Decodable {
         let basal: [BasalEntry]
         let sens: [SensEntry]
         let carbratio: [CarbRatioEntry]
-        let overrides: [OverrideEntry]?
         let target_high: [TargetEntry]?
         let target_low: [TargetEntry]?
         let timezone: String
@@ -61,8 +53,36 @@ struct NSProfile: Decodable {
         let percentage: Double?
         let target: Double?
     }
-    
+
+    struct LoopSettings: Decodable {
+        struct LoopOverridePreset: Decodable {
+            let name: String
+            let duration: Int?
+            let targetRange: [Double]?
+            let insulinNeedsScaleFactor: Double?
+            let symbol: String?
+        }
+
+        let deviceToken: String?
+        let bundleIdentifier: String?
+        let minimumBGGuard: Double?
+        let maximumBolus: Double?
+        let maximumBasalRatePerHour: Double?
+        let scheduleOverride: ScheduleOverride?
+        let dosingStrategy: String?
+        let overridePresets: [LoopOverridePreset]?
+        let dosingEnabled: Bool?
+
+        struct ScheduleOverride: Decodable {
+            let symbol: String?
+            let duration: Int?
+            let insulinNeedsScaleFactor: Double?
+            let name: String?
+        }
+    }
+
     let trioOverrides: [TrioOverrideEntry]?
+    let loopSettings: LoopSettings?
 
     enum CodingKeys: String, CodingKey {
         case store
@@ -72,6 +92,7 @@ struct NSProfile: Decodable {
         case isAPNSProduction
         case deviceToken
         case trioOverrides = "overridePresets"
+        case loopSettings = "loopSettings"
         case teamID
     }
 }

+ 19 - 14
LoopFollow/Controllers/Nightscout/ProfileManager.swift

@@ -16,7 +16,7 @@ final class ProfileManager {
     var carbRatioSchedule: [TimeValue<Double>]
     var targetLowSchedule: [TimeValue<HKQuantity>]
     var targetHighSchedule: [TimeValue<HKQuantity>]
-    var overrides: [Override]
+    var loopOverrides: [LoopOverride]
     var trioOverrides: [TrioOverride]
     var units: HKUnit
     var timezone: TimeZone
@@ -28,10 +28,10 @@ final class ProfileManager {
         let value: T
     }
 
-    struct Override {
+    struct LoopOverride {
         let name: String
         let targetRange: [HKQuantity]
-        let duration: Int
+        let duration: Int?
         let insulinNeedsScaleFactor: Double
         let symbol: String
     }
@@ -50,7 +50,7 @@ final class ProfileManager {
         self.carbRatioSchedule = []
         self.targetLowSchedule = []
         self.targetHighSchedule = []
-        self.overrides = []
+        self.loopOverrides = []
         self.trioOverrides = []
         self.units = .millimolesPerLiter
         self.timezone = TimeZone.current
@@ -74,16 +74,20 @@ final class ProfileManager {
         self.targetLowSchedule = store.target_low?.map { TimeValue(timeAsSeconds: Int($0.timeAsSeconds), value: HKQuantity(unit: self.units, doubleValue: $0.value)) } ?? []
         self.targetHighSchedule = store.target_high?.map { TimeValue(timeAsSeconds: Int($0.timeAsSeconds), value: HKQuantity(unit: self.units, doubleValue: $0.value)) } ?? []
 
-        if let overrides = store.overrides {
-            self.overrides = overrides.map { Override(
-                name: $0.name ?? "",
-                targetRange: $0.targetRange?.map { HKQuantity(unit: self.units, doubleValue: $0) } ?? [],
-                duration: $0.duration ?? 0,
-                insulinNeedsScaleFactor: $0.insulinNeedsScaleFactor ?? 1.0,
-                symbol: $0.symbol ?? ""
-            )}
+        if let loopSettings = profileData.loopSettings,
+           let overridePresets = loopSettings.overridePresets {
+            self.loopOverrides = overridePresets.map { preset in
+                let targetRangeQuantities = preset.targetRange?.map { HKQuantity(unit: self.units, doubleValue: $0) }
+                return LoopOverride(
+                    name: preset.name,
+                    targetRange: targetRangeQuantities ?? [],
+                    duration: preset.duration,
+                    insulinNeedsScaleFactor: preset.insulinNeedsScaleFactor ?? 1.0,
+                    symbol: preset.symbol ?? ""
+                )
+            }
         } else {
-            self.overrides = []
+            self.loopOverrides = []
         }
 
         if let trioOverrides = profileData.trioOverrides {
@@ -184,7 +188,8 @@ final class ProfileManager {
         self.carbRatioSchedule = []
         self.targetLowSchedule = []
         self.targetHighSchedule = []
-        self.overrides = []
+        self.loopOverrides = []
+        self.trioOverrides = []
         self.units = .millimolesPerLiter
         self.timezone = TimeZone.current
         self.defaultProfile = ""

+ 1 - 1
LoopFollow/Controllers/Nightscout/Treatments/Overrides.swift

@@ -65,7 +65,7 @@ extension MainViewController {
             }
 
             if dateTimeStamp <= now && now < endDate {
-                activeOverrideNote = currentEntry["notes"] as? String
+                activeOverrideNote = currentEntry["notes"] as? String ?? currentEntry["reason"] as? String
             }
 
             let dot = DataStructs.overrideStruct(insulNeedsScaleFactor: multiplier, date: dateTimeStamp, endDate: endDate, duration: duration, correctionRange: range, enteredBy: enteredBy, reason: reason, sgv: -20)

+ 76 - 13
LoopFollow/Helpers/NightscoutUtils.swift

@@ -47,6 +47,8 @@ class NightscoutUtils {
         case treatments
         case deviceStatus
         case iage = "Insulin Change"
+        case temporaryOverride = "Temporary Override"
+        case temporaryOverrideCancel = "Temporary Override Cancel"
 
         var endpoint: String {
             switch self {
@@ -58,6 +60,8 @@ class NightscoutUtils {
                 return "/api/v1/profile/current.json"
             case .deviceStatus:
                 return "/api/v1/devicestatus.json"
+            case .temporaryOverride, .temporaryOverrideCancel:
+                return "/api/v2/notifications/loop"
             }
         }
     }
@@ -167,27 +171,28 @@ class NightscoutUtils {
         return components?.url
     }
 
-    static func verifyURLAndToken(completion: @escaping (NightscoutError?, String?, Bool) -> Void) {
+    static func verifyURLAndToken(completion: @escaping (NightscoutError?, String?, Bool, Bool) -> Void) {
         let urlUser = ObservableUserDefaults.shared.url.value
         let token = UserDefaultsRepository.token.value
 
         if urlUser.isEmpty {
-            completion(.emptyAddress, nil, false)
+            completion(.emptyAddress, nil, false, false)
             return
         }
 
         guard let _ = URL(string: urlUser), urlUser.hasPrefix("http://") || urlUser.hasPrefix("https://") else {
-            completion(.invalidURL, nil, false)
+            completion(.invalidURL, nil, false, false)
             return
         }
 
         guard let request = createURLRequest(url: urlUser, token: token, path: "/api/v1/status.json") else {
-            completion(.invalidURL, nil, false)
+            completion(.invalidURL, nil, false, false)
             return
         }
 
         let task = URLSession.shared.dataTask(with: request) { data, response, error in
             var nsWriteAuth = false
+            var nsAdminAuth = false
 
             if let httpResponse = response as? HTTPURLResponse {
                 switch httpResponse.statusCode {
@@ -201,33 +206,34 @@ class NightscoutUtils {
 
                                 if permissionGroups.contains(where: { $0.contains("*") }) {
                                     nsWriteAuth = true
+                                    nsAdminAuth = true
                                 } else if permissionGroups.contains(where: { $0.contains("api:treatments:create") }) {
                                     nsWriteAuth = true
                                 }
-                                completion(nil, token, nsWriteAuth)
+                                completion(nil, token, nsWriteAuth, nsAdminAuth)
                             } else {
-                                completion(nil, nil, false)
+                                completion(nil, nil, false, false)
                             }
                         } catch {
-                            completion(nil, nil, false)
+                            completion(nil, nil, false, false)
                         }
                     } else {
-                        completion(nil, nil, false)
+                        completion(nil, nil, false, false)
                     }
                 case 401:
                     if token.isEmpty {
-                        completion(.tokenRequired, nil, false)
+                        completion(.tokenRequired, nil, false, false)
                     } else {
-                        completion(.invalidToken, nil, false)
+                        completion(.invalidToken, nil, false, false)
                     }
                 default:
-                    completion(.unknown, nil, false)
+                    completion(.unknown, nil, false, false)
                 }
             } else {
                 if let _ = error {
-                    completion(.siteNotFound, nil, false)
+                    completion(.siteNotFound, nil, false, false)
                 } else {
-                    completion(.networkError, nil, false)
+                    completion(.networkError, nil, false, false)
                 }
             }
         }
@@ -332,4 +338,61 @@ class NightscoutUtils {
         let decoder = JSONDecoder()
         return try decoder.decode(T.self, from: data)
     }
+
+    static func executePostRequest(eventType: EventType, body: [String: Any]) async throws -> String {
+        let jwtToken = try await retrieveJWTToken()
+        let baseURL = ObservableUserDefaults.shared.url.value
+
+        guard let url = URL(string: "\(baseURL)\(eventType.endpoint)") else {
+            throw NightscoutError.invalidURL
+        }
+
+        var request = URLRequest(url: url)
+        request.httpMethod = "POST"
+        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
+        request.setValue("Bearer \(jwtToken)", forHTTPHeaderField: "Authorization")
+        request.httpBody = try JSONSerialization.data(withJSONObject: body, options: [])
+
+        let sessionConfig = URLSessionConfiguration.default
+        sessionConfig.waitsForConnectivity = true
+        sessionConfig.networkServiceType = .responsiveData
+        let session = URLSession(configuration: sessionConfig)
+
+        let (data, response) = try await session.data(for: request)
+
+        var responseString : String
+        responseString = String(data: data, encoding: .utf8) ?? ""
+        guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
+            if responseString != "" {
+                return responseString
+            } else {
+                throw NightscoutError.networkError
+            }
+        }
+
+        return responseString
+    }
+
+    static func extractErrorReason(from responseString: String) -> String {
+        // 1) Try to parse the entire string as JSON and return the "message"
+        if let data = responseString.data(using: .utf8) {
+            if let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
+               let message = json["message"] as? String {
+                return message
+            }
+        }
+
+        // 2) If not valid JSON (or no "message"), try to parse it as HTML <title>
+        if let startRange = responseString.range(of: "<title>"),
+           let endRange = responseString.range(of: "</title>") {
+            let titleRange = startRange.upperBound..<endRange.lowerBound
+            let titleContent = responseString[titleRange].trimmingCharacters(in: .whitespacesAndNewlines)
+            if !titleContent.isEmpty {
+                return titleContent
+            }
+        }
+
+        // 3) Fallback: just return the entire raw string
+        return responseString
+    }
 }

+ 10 - 2
LoopFollow/Nightscout/NightscoutSettingsViewModel.swift

@@ -98,9 +98,10 @@ class NightscoutSettingsViewModel: ObservableObject {
     }
 
     func checkNightscoutStatus() {
-        NightscoutUtils.verifyURLAndToken { error, jwtToken, nsWriteAuth in
+        NightscoutUtils.verifyURLAndToken { error, jwtToken, nsWriteAuth, nsAdminAuth in
             DispatchQueue.main.async {
                 ObservableUserDefaults.shared.nsWriteAuth.value = nsWriteAuth
+                ObservableUserDefaults.shared.nsAdminAuth.value = nsAdminAuth
 
                 self.updateStatusLabel(error: error)
             }
@@ -126,7 +127,14 @@ class NightscoutSettingsViewModel: ObservableObject {
                 nightscoutStatus = "Address Empty"
             }
         } else {
-            nightscoutStatus = "OK (Read\(ObservableUserDefaults.shared.nsWriteAuth.value ? " & Write" : ""))"
+            let authStatus: String
+            if ObservableUserDefaults.shared.nsAdminAuth.value {
+                authStatus = "Admin"
+            } else {
+                authStatus = "Read" + (ObservableUserDefaults.shared.nsWriteAuth.value ? " & Write" : "")
+            }
+
+            nightscoutStatus = "OK (\(authStatus))"
 
             if (nightscoutURL != initialURL || nightscoutToken != initialToken) {
                 NotificationCenter.default.post(name: NSNotification.Name("refresh"), object: nil)

+ 38 - 0
LoopFollow/Remote/Loop/LoopNightscoutRemoteView.swift

@@ -0,0 +1,38 @@
+//
+//  LoopNightscoutRemoteView.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2025-01-14.
+//  Copyright © 2025 Jon Fawcett. All rights reserved.
+//
+
+import SwiftUI
+
+struct LoopNightscoutRemoteView: View {
+    @Environment(\.presentationMode) var presentationMode
+    @ObservedObject var nsAdmin = ObservableUserDefaults.shared.nsWriteAuth
+
+    var body: some View {
+        NavigationView {
+            if !nsAdmin.value {
+                ErrorMessageView(
+                    message: "Please update your token to include the 'admin' role in order to do remote commands with Loop."
+                )} else {
+                    VStack {
+                        let columns = [
+                            GridItem(.flexible(), spacing: 16),
+                            GridItem(.flexible(), spacing: 16)
+                        ]
+
+                        LazyVGrid(columns: columns, spacing: 16) {
+                            CommandButtonView(command: "Overrides", iconName: "slider.horizontal.3", destination: LoopOverrideView())
+                        }
+                        .padding(.horizontal)
+
+                        Spacer()
+                    }
+                    .navigationBarTitle("Loop Remote Control", displayMode: .inline)
+                }
+        }
+    }
+}

+ 226 - 0
LoopFollow/Remote/Loop/LoopOverrideView.swift

@@ -0,0 +1,226 @@
+//
+//  LoopOverrideView.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2025-01-14.
+//  Copyright © 2025 Jon Fawcett. All rights reserved.
+//
+
+import SwiftUI
+import HealthKit
+
+struct LoopOverrideView: View {
+    @Environment(\.presentationMode) private var presentationMode
+
+    @ObservedObject var device = ObservableUserDefaults.shared.device
+    @ObservedObject var overrideNote = Observable.shared.override
+    @ObservedObject var nsAdmin = ObservableUserDefaults.shared.nsWriteAuth
+
+    @StateObject private var viewModel = LoopOverrideViewModel()
+
+    @State private var showAlert: Bool = false
+    @State private var alertType: AlertType? = nil
+    @State private var alertMessage: String? = nil
+    @State private var isLoading: Bool = false
+    @State private var statusMessage: String? = nil
+
+    @State private var selectedOverride: ProfileManager.LoopOverride? = nil
+    @State private var showConfirmation: Bool = false
+
+    @FocusState private var noteFieldIsFocused: Bool
+
+    private let pushNotificationManager = PushNotificationManager()
+    private var profileManager = ProfileManager.shared
+
+    enum AlertType {
+        case confirmActivation
+        case confirmCancellation
+        case statusSuccess
+        case statusFailure
+        case validation
+    }
+
+    var body: some View {
+        NavigationView {
+            VStack {
+                if device.value != "Loop" {
+                    ErrorMessageView(
+                        message: "Remote commands are currently only available for Loop."
+                    )
+                } else if !nsAdmin.value {
+                    ErrorMessageView(
+                        message: "Please update your token to include the 'admin' role in order to do remote commands with Loop."
+                    )
+                } else {
+
+                    Form {
+                        if let activeNote = overrideNote.value {
+                            Section(header: Text("Active Override")) {
+                                HStack {
+                                    Text("Override")
+                                    Spacer()
+                                    Text(activeNote)
+                                        .foregroundColor(.secondary)
+                                }
+                                Button {
+                                    alertType = .confirmCancellation
+                                    showAlert = true
+                                } label: {
+                                    HStack {
+                                        Text("Cancel Override")
+                                        Spacer()
+                                        Image(systemName: "xmark.app")
+                                            .font(.title)
+                                    }
+                                }
+                                .tint(.red)
+                            }
+                            Section {
+                                Text("Setting a new override while one is active will cancel the active override and set the new one.")
+                                    .foregroundColor(.secondary)
+                            }
+                        }
+
+                        Section(header: Text("Available Overrides")) {
+                            if profileManager.loopOverrides.isEmpty {
+                                Text("No overrides available.")
+                                    .foregroundColor(.secondary)
+                            } else {
+                                ForEach(profileManager.loopOverrides, id: \.name) { override in
+                                    Button(action: {
+                                        selectedOverride = override
+                                        alertType = .confirmActivation
+                                        showAlert = true
+                                    }) {
+                                        HStack {
+                                            VStack(alignment: .leading) {
+                                                Text("\(override.symbol) \(override.name)")
+                                                    .font(.headline)
+                                                Text("Duration: \(formattedDuration(from: override.duration))")
+                                                    .font(.subheadline)
+                                                    .foregroundColor(.secondary)
+                                                Text("Insulin Scale Factor: \(String(format: "%.2f", override.insulinNeedsScaleFactor))")
+                                                    .font(.subheadline)
+                                                    .foregroundColor(.secondary)
+                                                if !override.targetRange.isEmpty {
+                                                    let range = override.targetRange.map { Localizer.formatQuantity($0) }.joined(separator: " - ")
+                                                    Text("Target Range: \(range) \(UserDefaultsRepository.getPreferredUnit().localizedShortUnitString)")
+                                                        .font(.subheadline)
+                                                        .foregroundColor(.secondary)
+                                                }
+                                            }
+                                            Spacer()
+                                            Image(systemName: "arrow.right.circle")
+                                                .foregroundColor(.blue)
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+
+                    if isLoading {
+                        ProgressView("Please wait...")
+                            .padding()
+                    }
+                }
+            }
+            .navigationTitle("Loop Overrides")
+            .navigationBarTitleDisplayMode(.inline)
+            .alert(isPresented: $showAlert) {
+                switch alertType {
+                case .confirmActivation:
+                    return Alert(
+                        title: Text("Activate Override"),
+                        message: Text("Do you want to activate the override '\(selectedOverride?.name ?? "")'?"),
+                        primaryButton: .default(Text("Confirm"), action: {
+                            if let override = selectedOverride {
+                                activateOverride(override: override)
+                            }
+                        }),
+                        secondaryButton: .cancel()
+                    )
+                case .confirmCancellation:
+                    return Alert(
+                        title: Text("Cancel Override"),
+                        message: Text("Are you sure you want to cancel the active override?"),
+                        primaryButton: .default(Text("Confirm"), action: {
+                            cancelOverride()
+                        }),
+                        secondaryButton: .cancel()
+                    )
+                case .statusSuccess:
+                    return Alert(
+                        title: Text("Success"),
+                        message: Text(statusMessage ?? ""),
+                        dismissButton: .default(Text("OK"), action: {
+                            presentationMode.wrappedValue.dismiss()
+                        })
+                    )
+                case .statusFailure:
+                    return Alert(
+                        title: Text("Error"),
+                        message: Text(statusMessage ?? "An error occurred."),
+                        dismissButton: .default(Text("OK"))
+                    )
+                case .validation:
+                    return Alert(
+                        title: Text("Validation Error"),
+                        message: Text(alertMessage ?? "Invalid input."),
+                        dismissButton: .default(Text("OK"))
+                    )
+                case .none:
+                    return Alert(title: Text("Unknown Alert"))
+                }
+            }
+        }
+    }
+
+    // MARK: - Functions
+    private func formattedDuration(from duration: Int?) -> String {
+        guard let duration = duration, duration != 0 else {
+            return "Indefinitely"
+        }
+
+        let hours = duration / 3600
+        let minutes = (duration % 3600) / 60
+
+        if hours > 0 && minutes > 0 {
+            return "\(hours) hr, \(minutes) min"
+        } else if hours > 0 {
+            return "\(hours) hr"
+        } else {
+            return "\(minutes) min"
+        }
+    }
+
+    private func activateOverride(override: ProfileManager.LoopOverride) {
+        isLoading = true
+        viewModel.sendActivateOverrideRequest(override: override) { success, message in
+            self.isLoading = false
+            if success {
+                self.statusMessage = "Override command sent successfully."
+                self.alertType = .statusSuccess
+            } else {
+                self.statusMessage = message ?? "Failed to send override command."
+                self.alertType = .statusFailure
+            }
+            self.showAlert = true
+        }
+    }
+
+    private func cancelOverride() {
+        isLoading = true
+        viewModel.sendCancelOverrideRequest { success, message in
+            self.isLoading = false
+            if success {
+                self.statusMessage = "Cancellation request successfully sent to Nightscout."
+                self.alertType = .statusSuccess
+            } else {
+                self.statusMessage = message ?? "Failed to cancel temp target."
+                self.alertType = .statusFailure
+            }
+            self.showAlert = true
+        }
+    }
+}

+ 82 - 0
LoopFollow/Remote/Loop/LoopOverrideViewModel.swift

@@ -0,0 +1,82 @@
+//
+//  LoopOverrideViewModel.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2025-01-15.
+//  Copyright © 2025 Jon Fawcett. All rights reserved.
+//
+
+import Foundation
+
+final class LoopOverrideViewModel: ObservableObject, Sendable {
+    func sendActivateOverrideRequest(
+        override: ProfileManager.LoopOverride,
+        completion: @escaping (Bool, String?) -> Void
+    ) {
+        Task {
+            let body: [String: Any] = [
+                "eventType": "Temporary Override",
+                "enteredBy": Storage.shared.user.value,
+                "reason": override.name,
+                "reasonDisplay": "\(override.symbol) \(override.name)"
+            ]
+
+            do {
+                let response: String = try await NightscoutUtils.executePostRequest(eventType: .temporaryOverride, body: body)
+                DispatchQueue.main.async {
+                    if response == "OK" {
+                        Observable.shared.override.value = nil
+                        NotificationCenter.default.post(name: NSNotification.Name("refresh"), object: nil)
+                        completion(true, response)
+                    } else {
+                        let errorTitle = NightscoutUtils.extractErrorReason(from: response)
+                        let formattedError = self.formatErrorMessage(errorTitle)
+                        completion(false, formattedError)
+                    }
+                }
+            } catch {
+                DispatchQueue.main.async {
+                    completion(false, nil)
+                }
+            }
+        }
+    }
+
+    func sendCancelOverrideRequest(completion: @escaping (Bool, String?) -> Void) {
+        Task {
+            let body: [String: Any] = [
+                "eventType": "Temporary Override Cancel"
+            ]
+
+            do {
+                let response: String = try await NightscoutUtils.executePostRequest(eventType: .temporaryOverrideCancel, body: body)
+                DispatchQueue.main.async {
+                    if response == "OK" {
+                        Observable.shared.override.value = nil
+                        NotificationCenter.default.post(name: NSNotification.Name("refresh"), object: nil)
+                        completion(true, response)
+                    } else {
+                        let errorTitle = NightscoutUtils.extractErrorReason(from: response)
+                        let formattedError = self.formatErrorMessage(errorTitle)
+                        completion(false, formattedError)
+                    }
+                }
+            } catch {
+                DispatchQueue.main.async {
+                    completion(false, nil)
+                }
+            }
+        }
+    }
+
+    func formatErrorMessage(_ errorTitle: String) -> String {
+        switch errorTitle {
+        case "Unauthorized":
+            return "Unauthorized, verify that your token is correct and has admin auth"
+        case "APNs delivery failed: BadDeviceToken":
+            return "APNs delivery failed: BadDeviceToken, verify that the LOOP_PUSH_SERVER_ENVIRONMENT parameter in your Nightscout setup matches the build method used for Loop"
+        default:
+            return errorTitle
+        }
+    }
+}

+ 1 - 1
LoopFollow/Remote/TrioNightscoutRemoteView.swift

@@ -52,7 +52,7 @@ struct TrioNightscoutRemoteView: View {
                     )
                 } else if !nsWriteAuth.value {
                     ErrorMessageView(
-                        message: "Please update your token to include the 'careportal' and 'readable' roles in order to do remote commands."
+                        message: "Please update your token to include the 'careportal' and 'readable' roles in order to do remote commands with Trio."
                     )
                 } else {
                     Form {

+ 23 - 0
LoopFollow/Remote/NoRemoteView.swift

@@ -0,0 +1,23 @@
+//
+//  NoRemoteView.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2025-01-14.
+//  Copyright © 2025 Jon Fawcett. All rights reserved.
+//
+
+import SwiftUI
+
+struct NoRemoteView: View {
+    private let remoteController = TrioNightscoutRemoteController()
+
+    var body: some View {
+        NavigationView {
+            VStack {
+                ErrorMessageView(
+                    message: "Remote commands are currently only available for Trio and Loop."
+                )
+            }
+        }
+    }
+}

+ 33 - 11
LoopFollow/Remote/RemoteViewController.swift

@@ -20,12 +20,15 @@ class RemoteViewController: UIViewController {
     override func viewDidLoad() {
         super.viewDidLoad()
 
-        cancellable = Storage.shared.remoteType.objectWillChange
-            .sink { [weak self] _ in
-                DispatchQueue.main.async {
-                    self?.updateView()
-                }
+        cancellable = Publishers.CombineLatest(
+            Storage.shared.remoteType.$value,
+            ObservableUserDefaults.shared.device.$value
+        )
+        .sink { [weak self] newRemoteType, newDevice in
+            DispatchQueue.main.async {
+                self?.updateView()
             }
+        }
 
         updateView()
     }
@@ -40,12 +43,30 @@ class RemoteViewController: UIViewController {
         }
 
         if remoteType == .nightscout {
-            let remoteView = TrioNightscoutRemoteView()
-            hostingController = UIHostingController(rootView: AnyView(remoteView))
+            var remoteView: AnyView
+
+            switch ObservableUserDefaults.shared.device.value {
+            case "Trio":
+                remoteView = AnyView(TrioNightscoutRemoteView())
+            case "Loop":
+                remoteView = AnyView(LoopNightscoutRemoteView())
+            default:
+                remoteView = AnyView(NoRemoteView())
+            }
+
+            hostingController = UIHostingController(rootView: remoteView)
         } else if remoteType == .trc {
-            let trioRemoteControlViewModel = TrioRemoteControlViewModel()
-            let trioRemoteControlView = TrioRemoteControlView(viewModel: trioRemoteControlViewModel)
-            hostingController = UIHostingController(rootView: AnyView(trioRemoteControlView))
+            if ObservableUserDefaults.shared.device.value != "Trio" {
+                hostingController = UIHostingController(
+                    rootView: AnyView(
+                        Text("Trio Remote Control is only supported for 'Trio'")
+                    )
+                )
+            } else {
+                let trioRemoteControlViewModel = TrioRemoteControlViewModel()
+                let trioRemoteControlView = TrioRemoteControlView(viewModel: trioRemoteControlViewModel)
+                hostingController = UIHostingController(rootView: AnyView(trioRemoteControlView))
+            }
         } else {
             hostingController = UIHostingController(rootView: AnyView(Text("Please select a Remote Type in Settings.")))
         }
@@ -66,9 +87,10 @@ class RemoteViewController: UIViewController {
         }
 
         if remoteType == .nightscout, !ObservableUserDefaults.shared.nsWriteAuth.value {
-            NightscoutUtils.verifyURLAndToken { error, jwtToken, nsWriteAuth in
+            NightscoutUtils.verifyURLAndToken { error, jwtToken, nsWriteAuth, nsAdminAuth in
                 DispatchQueue.main.async {
                     ObservableUserDefaults.shared.nsWriteAuth.value = nsWriteAuth
+                    ObservableUserDefaults.shared.nsAdminAuth.value = nsAdminAuth
                 }
             }
         }

+ 46 - 38
LoopFollow/Remote/Settings/RemoteSettingsView.swift

@@ -13,23 +13,11 @@ import HealthKit
 struct RemoteSettingsView: View {
     @ObservedObject var viewModel: RemoteSettingsViewModel
     @Environment(\.presentationMode) var presentationMode
-    @FocusState private var focusedField: Field?
 
     @State private var showAlert: Bool = false
     @State private var alertType: AlertType? = nil
     @State private var alertMessage: String? = nil
 
-    enum Field: Hashable {
-        case user
-        case deviceToken
-        case sharedSecret
-        case apnsKey
-        case teamId
-        case keyId
-        case bundleId
-        case maxBolus
-    }
-
     enum AlertType {
         case validation
     }
@@ -37,27 +25,32 @@ struct RemoteSettingsView: View {
     var body: some View {
         NavigationView {
             Form {
-                // Remote Type Section
-                // Instructions for Remote Type options
-                Section {
-                    Picker("Remote Type", selection: $viewModel.remoteType) {
-                        Text("None").tag(RemoteType.none)
-                        Text("Nightscout").tag(RemoteType.nightscout)
-                        if BuildDetails.default.branch?.lowercased() != "main" {
-                            Text("Trio Remote Control").tag(RemoteType.trc)
-                        }
+                // MARK: - Remote Type Section (Custom Rows)
+                Section(header: Text("Remote Type")) {
+                    remoteTypeRow(type: .none, label: "None", isEnabled: true)
+
+                    remoteTypeRow(type: .nightscout, label: "Nightscout", isEnabled: true)
+
+                    if BuildDetails.default.branch?.lowercased() != "main" {
+                        remoteTypeRow(
+                            type: .trc,
+                            label: "Trio Remote Control",
+                            isEnabled: viewModel.isTrioDevice
+                        )
                     }
-                    .pickerStyle(MenuPickerStyle())
 
-                    Text("Nightscout is the only option available for the released version of Trio.")
-                        .font(.footnote)
-                        .foregroundColor(.secondary)
-                    Text("Trio Remote Control requires a special version of Trio, which is under development in a private repository until sufficient testing is completed.")
+                    Text("Nightscout is the only option for the released version of Trio and Loop.")
                         .font(.footnote)
                         .foregroundColor(.secondary)
+
+                    if BuildDetails.default.branch?.lowercased() != "main" {
+                        Text("Trio Remote Control requires a special version of Trio, which is under development in a private repository until sufficient testing is completed.")
+                            .font(.footnote)
+                            .foregroundColor(.secondary)
+                    }
                 }
 
-                // User Information Section
+                // MARK: - User Information Section
                 if viewModel.remoteType != .none {
                     Section(header: Text("User Information")) {
                         HStack {
@@ -65,13 +58,12 @@ struct RemoteSettingsView: View {
                             TextField("Enter User", text: $viewModel.user)
                                 .autocapitalization(.none)
                                 .disableAutocorrection(true)
-                                .focused($focusedField, equals: .user)
                                 .multilineTextAlignment(.trailing)
                         }
                     }
                 }
 
-                // Trio Remote Control Settings Section
+                // MARK: - Trio Remote Control Settings
                 if viewModel.remoteType == .trc {
                     Section(header: Text("Trio Remote Control Settings")) {
                         HStack {
@@ -79,7 +71,6 @@ struct RemoteSettingsView: View {
                             TextField("Enter Shared Secret", text: $viewModel.sharedSecret)
                                 .autocapitalization(.none)
                                 .disableAutocorrection(true)
-                                .focused($focusedField, equals: .sharedSecret)
                                 .multilineTextAlignment(.trailing)
                         }
 
@@ -88,7 +79,6 @@ struct RemoteSettingsView: View {
                             TextField("Enter APNS Key ID", text: $viewModel.keyId)
                                 .autocapitalization(.none)
                                 .disableAutocorrection(true)
-                                .focused($focusedField, equals: .keyId)
                                 .multilineTextAlignment(.trailing)
                         }
 
@@ -102,12 +92,10 @@ struct RemoteSettingsView: View {
                                     RoundedRectangle(cornerRadius: 8)
                                         .stroke(Color.gray.opacity(0.5), lineWidth: 1)
                                 )
-                                .focused($focusedField, equals: .apnsKey)
                         }
-
                     }
 
-                    // Guardrails Section
+                    // MARK: - Guardrails
                     Section(header: Text("Guardrails")) {
                         HStack {
                             Text("Max Bolus")
@@ -186,7 +174,7 @@ struct RemoteSettingsView: View {
                         }
                     }
 
-                    // Meal Section
+                    // MARK: - Meal Section
                     Section(header: Text("Meal Settings")) {
                         Toggle("Meal with Bolus", isOn: $viewModel.mealWithBolus)
                             .toggleStyle(SwitchToggleStyle())
@@ -195,6 +183,7 @@ struct RemoteSettingsView: View {
                             .toggleStyle(SwitchToggleStyle())
                     }
 
+                    // MARK: - Debug / Info
                     Section(header: Text("Debug / Info")) {
                         Text("Device Token: \(Storage.shared.deviceToken.value)")
                         Text("Production Env.: \(Storage.shared.productionEnvironment.value ? "True" : "False")")
@@ -211,9 +200,6 @@ struct RemoteSettingsView: View {
                     }
                 }
             }
-            .onTapGesture {
-                focusedField = nil
-            }
             .alert(isPresented: $showAlert) {
                 switch alertType {
                 case .validation:
@@ -229,6 +215,28 @@ struct RemoteSettingsView: View {
         }
     }
 
+    // MARK: - Custom Row for Remote Type Selection
+    private func remoteTypeRow(type: RemoteType, label: String, isEnabled: Bool) -> some View {
+        Button(action: {
+            if isEnabled {
+                viewModel.remoteType = type
+            }
+        }) {
+            HStack {
+                Text(label)
+                Spacer()
+                if viewModel.remoteType == type {
+                    Image(systemName: "checkmark")
+                        .foregroundColor(.accentColor)
+                }
+            }
+        }
+        // If isEnabled is false, user can see the row but not tap it.
+        .disabled(!isEnabled)
+        .foregroundColor(isEnabled ? .primary : .gray)
+    }
+
+    // MARK: - Validation Error Handler
     private func handleValidationError(_ message: String) {
         alertMessage = message
         alertType = .validation

+ 8 - 0
LoopFollow/Remote/Settings/RemoteSettingsViewModel.swift

@@ -23,6 +23,7 @@ class RemoteSettingsViewModel: ObservableObject {
     @Published var maxFat: HKQuantity
     @Published var mealWithBolus: Bool
     @Published var mealWithFatProtein: Bool
+    @Published var isTrioDevice: Bool = (ObservableUserDefaults.shared.device.value == "Trio")
 
     private var storage = Storage.shared
     private var cancellables = Set<AnyCancellable>()
@@ -87,5 +88,12 @@ class RemoteSettingsViewModel: ObservableObject {
         $mealWithFatProtein
             .sink { [weak self] in self?.storage.mealWithFatProtein.value = $0 }
             .store(in: &cancellables)
+
+        ObservableUserDefaults.shared.device.$value
+            .receive(on: DispatchQueue.main            )
+            .sink { [weak self] newValue in
+                self?.isTrioDevice = (newValue == "Trio")
+            }
+            .store(in: &cancellables)
     }
 }

LoopFollow/Remote/PushMessage.swift → LoopFollow/Remote/TRC/PushMessage.swift


LoopFollow/Remote/PushNotificationManager.swift → LoopFollow/Remote/TRC/PushNotificationManager.swift


LoopFollow/Remote/TreatmentResponse.swift → LoopFollow/Remote/TRC/TreatmentResponse.swift


+ 1 - 1
LoopFollow/Remote/TrioNightscoutRemoteController.swift.swift

@@ -1,5 +1,5 @@
 //
-//  TrioNightscoutRemoteController.swift.swift
+//  TrioNightscoutRemoteController.swift
 //  LoopFollow
 //
 //  Created by Jonas Björkert on 2024-08-26.

LoopFollow/Remote/TrioRemoteControlView.swift → LoopFollow/Remote/TRC/TrioRemoteControlView.swift


LoopFollow/Remote/TrioRemoteControlViewModel.swift → LoopFollow/Remote/TRC/TrioRemoteControlViewModel.swift


+ 1 - 0
LoopFollow/Storage/ObservableUserDefaults.swift

@@ -15,6 +15,7 @@ class ObservableUserDefaults {
     var url = ObservableUserDefaultsValue<String>(key: "url", default: "")
     var device = ObservableUserDefaultsValue<String>(key: "device", default: "")
     var nsWriteAuth = ObservableUserDefaultsValue<Bool>(key: "nsWriteAuth", default: false)
+    var nsAdminAuth = ObservableUserDefaultsValue<Bool>(key: "nsAdminAuth", default: false)
 
     var contactEnabled = ObservableUserDefaultsValue<Bool>(key: "contactEnabled", default: false)
     var contactTrend = ObservableUserDefaultsValue<Bool>(key: "contactTrend", default: false)

+ 4 - 0
LoopFollow/ViewControllers/MainViewController.swift

@@ -147,6 +147,10 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
     override func viewDidLoad() {
         super.viewDidLoad()
 
+        if ObservableUserDefaults.shared.device.value != "Trio" && Storage.shared.remoteType.value == .trc {
+            Storage.shared.remoteType.value = .none
+        }
+
         //Migration of UserDefaultsRepository -> Storage handling
         if !UserDefaultsRepository.backgroundRefresh.value {
             Storage.shared.backgroundRefreshType.value = .none