Procházet zdrojové kódy

Ivan's xdrip support

Jon Mårtensson před 4 roky
rodič
revize
976a703a22

+ 56 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -12,6 +12,14 @@
 		0D9A5E34A899219C5C4CDFAF /* DataTableViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9455FA2D92E77A6C4AFED8A3 /* DataTableViewModel.swift */; };
 		17A9D0899046B45E87834820 /* CREditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C8D5F457B5AFF763F8CF3DF /* CREditorProvider.swift */; };
 		19434C14DF3F4816F4E4BF2E /* BolusBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77FAEF7B34EEC71B3A7B800C /* BolusBuilder.swift */; };
+		1997329927159BA100129A3F /* AppGroupSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1997329827159BA100129A3F /* AppGroupSource.swift */; };
+		1997329B27159BB800129A3F /* CGMType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1997329A27159BB800129A3F /* CGMType.swift */; };
+		1997329D27159BCD00129A3F /* GlucoseSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1997329C27159BCD00129A3F /* GlucoseSource.swift */; };
+		199732A327159C9D00129A3F /* CGMBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 199732A227159C9D00129A3F /* CGMBuilder.swift */; };
+		199732A527159CAF00129A3F /* CGMDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 199732A427159CAF00129A3F /* CGMDataFlow.swift */; };
+		199732A727159CC400129A3F /* CGMProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 199732A627159CC400129A3F /* CGMProvider.swift */; };
+		199732A927159CE100129A3F /* CGMViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 199732A827159CE100129A3F /* CGMViewModel.swift */; };
+		199732AC27159D1D00129A3F /* CGMRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 199732AB27159D1D00129A3F /* CGMRootView.swift */; };
 		199D0AC02670351400D24E2B /* Descriptions.strings in Resources */ = {isa = PBXBuildFile; fileRef = 199D0AC22670351400D24E2B /* Descriptions.strings */; };
 		19A9A6FC2666335E005378EB /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 19A9A6FE2666335E005378EB /* Localizable.strings */; };
 		1BBB001DAD60F3B8CEA4B1C7 /* ISFEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505E09DC17A0C3D0AF4B66FE /* ISFEditorViewModel.swift */; };
@@ -318,6 +326,14 @@
 		12204445D7632AF09264A979 /* PreferencesEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PreferencesEditorDataFlow.swift; sourceTree = "<group>"; };
 		198C700C26703D8600DDDAF0 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = "<group>"; };
 		198C700F26703E6200DDDAF0 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Descriptions.strings; sourceTree = "<group>"; };
+		1997329827159BA100129A3F /* AppGroupSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppGroupSource.swift; sourceTree = "<group>"; };
+		1997329A27159BB800129A3F /* CGMType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGMType.swift; sourceTree = "<group>"; };
+		1997329C27159BCD00129A3F /* GlucoseSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseSource.swift; sourceTree = "<group>"; };
+		199732A227159C9D00129A3F /* CGMBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGMBuilder.swift; sourceTree = "<group>"; };
+		199732A427159CAF00129A3F /* CGMDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGMDataFlow.swift; sourceTree = "<group>"; };
+		199732A627159CC400129A3F /* CGMProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGMProvider.swift; sourceTree = "<group>"; };
+		199732A827159CE100129A3F /* CGMViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGMViewModel.swift; sourceTree = "<group>"; };
+		199732AB27159D1D00129A3F /* CGMRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGMRootView.swift; sourceTree = "<group>"; };
 		199D0AC12670351400D24E2B /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Descriptions.strings; sourceTree = "<group>"; };
 		199D0AC32670351800D24E2B /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Descriptions.strings; sourceTree = "<group>"; };
 		199D0AC42670351900D24E2B /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Descriptions.strings"; sourceTree = "<group>"; };
@@ -662,6 +678,36 @@
 			path = View;
 			sourceTree = "<group>";
 		};
+		1997329727159B4F00129A3F /* CGM */ = {
+			isa = PBXGroup;
+			children = (
+				1997329827159BA100129A3F /* AppGroupSource.swift */,
+				1997329A27159BB800129A3F /* CGMType.swift */,
+				1997329C27159BCD00129A3F /* GlucoseSource.swift */,
+			);
+			path = CGM;
+			sourceTree = "<group>";
+		};
+		1997329E27159BEF00129A3F /* CGM */ = {
+			isa = PBXGroup;
+			children = (
+				199732A227159C9D00129A3F /* CGMBuilder.swift */,
+				199732A427159CAF00129A3F /* CGMDataFlow.swift */,
+				199732A627159CC400129A3F /* CGMProvider.swift */,
+				199732A827159CE100129A3F /* CGMViewModel.swift */,
+				199732AA27159CFE00129A3F /* View */,
+			);
+			path = CGM;
+			sourceTree = "<group>";
+		};
+		199732AA27159CFE00129A3F /* View */ = {
+			isa = PBXGroup;
+			children = (
+				199732AB27159D1D00129A3F /* CGMRootView.swift */,
+			);
+			path = View;
+			sourceTree = "<group>";
+		};
 		199D0ABC267031DC00D24E2B /* Main */ = {
 			isa = PBXGroup;
 			children = (
@@ -698,6 +744,7 @@
 		3811DE0325C9D31700A708ED /* Modules */ = {
 			isa = PBXGroup;
 			children = (
+				1997329E27159BEF00129A3F /* CGM */,
 				6DC5D590658EF8B8DF94F9F5 /* AddCarbs */,
 				A9A4C88374496B3C89058A89 /* AddTempTarget */,
 				3811DE4525C9D4B800A708ED /* AuthorizedRoot */,
@@ -1034,6 +1081,7 @@
 		3811DF0A25CAAAA500A708ED /* APS */ = {
 			isa = PBXGroup;
 			children = (
+				1997329727159B4F00129A3F /* CGM */,
 				3811DF0F25CAAAE200A708ED /* APSManager.swift */,
 				38BF021E25E7F0DE00579895 /* DeviceDataManager.swift */,
 				38A43597262E0E4900E80935 /* FetchAnnouncementsManager.swift */,
@@ -1718,6 +1766,7 @@
 				38C4D33725E9A1A300D30B77 /* DispatchQueue+Extensions.swift in Sources */,
 				3811DE6B25C9D62600A708ED /* OnboardingProvider.swift in Sources */,
 				38B4F3C325E2A20B00E76A18 /* PumpSetupView.swift in Sources */,
+				1997329927159BA100129A3F /* AppGroupSource.swift in Sources */,
 				382C134B25F14E3700715CE1 /* BGTargets.swift in Sources */,
 				38AEE75725F0F18E0013F05B /* CarbsStorage.swift in Sources */,
 				38B4F3CA25E502E200E76A18 /* SwiftNotificationCenter.swift in Sources */,
@@ -1728,6 +1777,7 @@
 				3895E4C625B9E00D00214B37 /* Preferences.swift in Sources */,
 				3811DE6E25C9D62600A708ED /* OnboardingViewModel.swift in Sources */,
 				38B4F3CD25E5031100E76A18 /* Broadcaster.swift in Sources */,
+				199732A527159CAF00129A3F /* CGMDataFlow.swift in Sources */,
 				3811DE6D25C9D62600A708ED /* OnboardingRootView.swift in Sources */,
 				383420D925FFEB3F002D46C1 /* Popup.swift in Sources */,
 				3811DE3025C9D49500A708ED /* HomeViewModel.swift in Sources */,
@@ -1741,6 +1791,7 @@
 				3811DE1725C9D40400A708ED /* Screen.swift in Sources */,
 				383948DA25CD64D500E91849 /* Glucose.swift in Sources */,
 				388E596C25AD95110019842D /* OpenAPS.swift in Sources */,
+				199732A327159C9D00129A3F /* CGMBuilder.swift in Sources */,
 				384E803825C388640086DB71 /* Script.swift in Sources */,
 				3811DE0925C9D32F00A708ED /* BaseViewModel.swift in Sources */,
 				3883583425EEB38000E024B2 /* PumpSettings.swift in Sources */,
@@ -1790,6 +1841,7 @@
 				3811DE5C25C9D4D500A708ED /* Formatters.swift in Sources */,
 				3811DEC525C9D99900A708ED /* StorageContainer.swift in Sources */,
 				3871F39F25ED895A0013ECB5 /* Decimal+Extensions.swift in Sources */,
+				1997329D27159BCD00129A3F /* GlucoseSource.swift in Sources */,
 				3811DE7F25C9D6D300A708ED /* LoginBuilder.swift in Sources */,
 				3811DE3525C9D49500A708ED /* HomeRootView.swift in Sources */,
 				3811DEC325C9D99900A708ED /* UIContainer.swift in Sources */,
@@ -1848,6 +1900,7 @@
 				642F76A05A4FF530463A9FD0 /* NightscoutConfigRootView.swift in Sources */,
 				3340E0D14D4701342D459C95 /* PumpConfigBuilder.swift in Sources */,
 				AD3D2CD42CD01B9EB8F26522 /* PumpConfigDataFlow.swift in Sources */,
+				1997329B27159BB800129A3F /* CGMType.swift in Sources */,
 				53F2382465BF74DB1A967C8B /* PumpConfigProvider.swift in Sources */,
 				5D16287A969E64D18CE40E44 /* PumpConfigViewModel.swift in Sources */,
 				E974172296125A5AE99E634C /* PumpConfigRootView.swift in Sources */,
@@ -1875,6 +1928,8 @@
 				DBA5254DBB2586C98F61220C /* ISFEditorProvider.swift in Sources */,
 				1BBB001DAD60F3B8CEA4B1C7 /* ISFEditorViewModel.swift in Sources */,
 				38192E0D261BAF980094D973 /* ConvenienceExtensions.swift in Sources */,
+				199732AC27159D1D00129A3F /* CGMRootView.swift in Sources */,
+				199732A727159CC400129A3F /* CGMProvider.swift in Sources */,
 				88AB39B23C9552BD6E0C9461 /* ISFEditorRootView.swift in Sources */,
 				3BD663A04B4CA5278B0260B4 /* CREditorBuilder.swift in Sources */,
 				A33352ED40476125EBAC6EE0 /* CREditorDataFlow.swift in Sources */,
@@ -1902,6 +1957,7 @@
 				D2165E9D78EFF692C1DED1C6 /* AddTempTargetDataFlow.swift in Sources */,
 				5BFA1C2208114643B77F8CEB /* AddTempTargetProvider.swift in Sources */,
 				919DBD08F13BAFB180DF6F47 /* AddTempTargetViewModel.swift in Sources */,
+				199732A927159CE100129A3F /* CGMViewModel.swift in Sources */,
 				8BC2F5A29AD1ED08AC0EE013 /* AddTempTargetRootView.swift in Sources */,
 				38A00B1F25FC00F7006BC0B0 /* Autotune.swift in Sources */,
 				19434C14DF3F4816F4E4BF2E /* BolusBuilder.swift in Sources */,

+ 6 - 6
FreeAPS.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -6,8 +6,8 @@
         "repositoryURL": "https://github.com/Alamofire/Alamofire",
         "state": {
           "branch": null,
-          "revision": "4d19ad82f80cc71ff829b941ded114c56f4f604c",
-          "version": "5.4.2"
+          "revision": "d120af1e8638c7da36c8481fd61a66c0c08dc4fc",
+          "version": "5.4.4"
         }
       },
       {
@@ -24,8 +24,8 @@
         "repositoryURL": "https://github.com/apple/swift-algorithms",
         "state": {
           "branch": null,
-          "revision": "04d91803a2dd58c14db9cf66c7412c6dd4a26fc8",
-          "version": "0.1.1"
+          "revision": "2327673b0e9c7e90e6b1826376526ec3627210e4",
+          "version": "0.2.1"
         }
       },
       {
@@ -51,8 +51,8 @@
         "repositoryURL": "https://github.com/Swinject/Swinject",
         "state": {
           "branch": null,
-          "revision": "8a76d2c74bafbb455763487cc6a08e91bad1f78b",
-          "version": "2.7.1"
+          "revision": "f10b6e9ebff440f985c43008f7c2d097639fcb81",
+          "version": "2.8.1"
         }
       }
     ]

+ 2 - 0
FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json

@@ -10,4 +10,6 @@
     "debugOptions": false,
     "insulinReqFraction": 0.7,
     "skipBolusScreenAfterCarbs": false
+    "skipBolusScreenAfterCarbs": false,
+    "cgm": "nightscout"
 }

+ 68 - 0
FreeAPS/Sources/APS/CGM/AppGroupSource.swift

@@ -0,0 +1,68 @@
+import Combine
+import Foundation
+
+struct AppGroupSource: GlucoseSource {
+    func fetch() -> AnyPublisher<[BloodGlucose], Never> {
+        guard let suiteName = Bundle.main.appGroupSuiteName,
+              let sharedDefaults = UserDefaults(suiteName: suiteName)
+        else {
+            return Just([]).eraseToAnyPublisher()
+        }
+
+        return Just(fetchLastBGs(60, sharedDefaults)).eraseToAnyPublisher()
+    }
+
+    private func fetchLastBGs(_ count: Int, _ sharedDefaults: UserDefaults) -> [BloodGlucose] {
+        guard let sharedData = sharedDefaults.data(forKey: "latestReadings") else {
+            return []
+        }
+
+        let decoded = try? JSONSerialization.jsonObject(with: sharedData, options: [])
+        guard let sgvs = decoded as? [AnyObject] else {
+            return []
+        }
+
+        var results: [BloodGlucose] = []
+        for sgv in sgvs.prefix(count) {
+            guard
+                let glucose = sgv["Value"] as? Int,
+                let direction = sgv["direction"] as? String,
+                let timestamp = sgv["DT"] as? String,
+                let date = parseDate(timestamp)
+            else { continue }
+
+            results.append(
+                BloodGlucose(
+                    _id: UUID().uuidString,
+                    sgv: glucose,
+                    direction: BloodGlucose.Direction(rawValue: direction),
+                    date: Decimal(Int(date.timeIntervalSince1970 * 1000)),
+                    dateString: date,
+                    filtered: nil,
+                    noise: nil,
+                    glucose: glucose
+                )
+            )
+        }
+        return results
+    }
+
+    private func parseDate(_ timestamp: String) -> Date? {
+        // timestamp looks like "/Date(1462404576000)/"
+        guard let re = try? NSRegularExpression(pattern: "\\((.*)\\)"),
+              let match = re.firstMatch(in: timestamp, range: NSMakeRange(0, timestamp.count))
+        else {
+            return nil
+        }
+
+        let matchRange = match.range(at: 1)
+        let epoch = Double((timestamp as NSString).substring(with: matchRange))! / 1000
+        return Date(timeIntervalSince1970: epoch)
+    }
+}
+
+public extension Bundle {
+    var appGroupSuiteName: String? {
+        object(forInfoDictionaryKey: "AppGroupID") as? String
+    }
+}

+ 20 - 0
FreeAPS/Sources/APS/CGM/CGMType.swift

@@ -0,0 +1,20 @@
+import Foundation
+
+enum CGMType: String, JSON, CaseIterable, Identifiable {
+    var id: String { rawValue }
+
+    case nightscout
+    case xdrip
+//    case dexcom
+
+    var displayName: String {
+        switch self {
+        case .nightscout:
+            return "Nightscout"
+        case .xdrip:
+            return "xDrip"
+        }
+    }
+
+    static var allCases: [CGMType] = [.nightscout, .xdrip]
+}

+ 5 - 0
FreeAPS/Sources/APS/CGM/GlucoseSource.swift

@@ -0,0 +1,5 @@
+import Combine
+
+protocol GlucoseSource {
+    func fetch() -> AnyPublisher<[BloodGlucose], Never>
+}

+ 13 - 70
FreeAPS/Sources/APS/FetchGlucoseManager.swift

@@ -10,15 +10,27 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
     @Injected() var glucoseStorage: GlucoseStorage!
     @Injected() var nightscoutManager: NightscoutManager!
     @Injected() var apsManager: APSManager!
+    @Injected() var settingsManager: SettingsManager!
 
     private var lifetime = Lifetime()
     private let timer = DispatchTimer(timeInterval: 1.minutes.timeInterval)
 
+    private lazy var appGroupSource = AppGroupSource()
+
     init(resolver: Resolver) {
         injectServices(resolver)
         subscribe()
     }
 
+    var glucoseSource: AnyPublisher<[BloodGlucose], Never> {
+        switch settingsManager.settings.cgm {
+        case .xdrip:
+            return appGroupSource.fetch()
+        default:
+            return nightscoutManager.fetchGlucose()
+        }
+    }
+
     private func subscribe() {
         timer.publisher
             .receive(on: processQueue)
@@ -28,12 +40,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
                 return Publishers.CombineLatest3(
                     Just(date),
                     Just(self.glucoseStorage.syncDate()),
-                    Publishers.CombineLatest(
-                        self.nightscoutManager.fetchGlucose(),
-                        self.fetchGlucoseFromSharedGroup()
-                    )
-                    .map { [$0, $1].flatMap { $0 } }
-                    .eraseToAnyPublisher()
+                    self.glucoseSource
                 )
                 .eraseToAnyPublisher()
             }
@@ -51,68 +58,4 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         timer.fire()
         timer.resume()
     }
-
-    private func fetchGlucoseFromSharedGroup() -> AnyPublisher<[BloodGlucose], Never> {
-        guard let suiteName = Bundle.main.appGroupSuiteName,
-              let sharedDefaults = UserDefaults(suiteName: suiteName)
-        else {
-            return Just([]).eraseToAnyPublisher()
-        }
-
-        return Just(fetchLastBGs(60, sharedDefaults)).eraseToAnyPublisher()
-    }
-
-    private func fetchLastBGs(_ count: Int, _ sharedDefaults: UserDefaults) -> [BloodGlucose] {
-        guard let sharedData = sharedDefaults.data(forKey: "latestReadings") else {
-            return []
-        }
-
-        let decoded = try? JSONSerialization.jsonObject(with: sharedData, options: [])
-        guard let sgvs = decoded as? [AnyObject] else {
-            return []
-        }
-
-        var results: [BloodGlucose] = []
-        for sgv in sgvs.prefix(count) {
-            guard
-                let glucose = sgv["Value"] as? Int,
-                let direction = sgv["direction"] as? String,
-                let timestamp = sgv["DT"] as? String,
-                let date = parseDate(timestamp)
-            else { continue }
-
-            results.append(
-                BloodGlucose(
-                    _id: UUID().uuidString,
-                    sgv: glucose,
-                    direction: BloodGlucose.Direction(rawValue: direction),
-                    date: Decimal(Int(date.timeIntervalSince1970 * 1000)),
-                    dateString: date,
-                    filtered: nil,
-                    noise: nil,
-                    glucose: glucose
-                )
-            )
-        }
-        return results
-    }
-
-    private func parseDate(_ timestamp: String) -> Date? {
-        // timestamp looks like "/Date(1462404576000)/"
-        guard let re = try? NSRegularExpression(pattern: "\\((.*)\\)"),
-              let match = re.firstMatch(in: timestamp, range: NSMakeRange(0, timestamp.count))
-        else {
-            return nil
-        }
-
-        let matchRange = match.range(at: 1)
-        let epoch = Double((timestamp as NSString).substring(with: matchRange))! / 1000
-        return Date(timeIntervalSince1970: epoch)
-    }
-}
-
-public extension Bundle {
-    var appGroupSuiteName: String? {
-        object(forInfoDictionaryKey: "AppGroupID") as? String
-    }
 }

+ 1 - 0
FreeAPS/Sources/Models/FreeAPSSettings.swift

@@ -12,4 +12,5 @@ struct FreeAPSSettings: JSON, Equatable {
     var debugOptions: Bool?
     var insulinReqFraction: Decimal?
     var skipBolusScreenAfterCarbs: Bool?
+    var cgm: CGMType?
 }

+ 3 - 0
FreeAPS/Sources/Modules/CGM/CGMBuilder.swift

@@ -0,0 +1,3 @@
+extension CGM {
+    final class Builder: BaseModuleBuilder<RootView, ViewModel<Provider>, Provider> {}
+}

+ 5 - 0
FreeAPS/Sources/Modules/CGM/CGMDataFlow.swift

@@ -0,0 +1,5 @@
+enum CGM {
+    enum Config {}
+}
+
+protocol CGMProvider: Provider {}

+ 3 - 0
FreeAPS/Sources/Modules/CGM/CGMProvider.swift

@@ -0,0 +1,3 @@
+extension CGM {
+    final class Provider: BaseProvider, CGMProvider {}
+}

+ 20 - 0
FreeAPS/Sources/Modules/CGM/CGMViewModel.swift

@@ -0,0 +1,20 @@
+import SwiftUI
+
+extension CGM {
+    class ViewModel<Provider>: BaseViewModel<Provider>, ObservableObject where Provider: CGMProvider {
+        @Injected() var settingsManager: SettingsManager!
+
+        @Published var cgm: CGMType = .nightscout
+
+        override func subscribe() {
+            cgm = settingsManager.settings.cgm ?? .nightscout
+
+            $cgm
+                .removeDuplicates()
+                .sink { [weak self] value in
+                    self?.settingsManager.settings.cgm = value
+                }
+                .store(in: &lifetime)
+        }
+    }
+}

+ 21 - 0
FreeAPS/Sources/Modules/CGM/View/CGMRootView.swift

@@ -0,0 +1,21 @@
+import SwiftUI
+
+extension CGM {
+    struct RootView: BaseView {
+        @EnvironmentObject var viewModel: ViewModel<Provider>
+
+        var body: some View {
+            Form {
+                Section {
+                    Picker("Type", selection: $viewModel.cgm) {
+                        ForEach(CGMType.allCases) {
+                            Text($0.displayName).tag($0)
+                        }
+                    }
+                }
+            }
+            .navigationTitle("CGM")
+            .navigationBarTitleDisplayMode(.automatic)
+        }
+    }
+}

+ 1 - 0
FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift

@@ -17,6 +17,7 @@ extension Settings {
 
                 Section(header: Text("Services")) {
                     Text("Nightscout").navigationLink(to: .nighscoutConfig, from: self)
+                    Text("CGM").navigationLink(to: .cgm, from: self)
                 }
 
                 Section(header: Text("Configuration")) {

+ 3 - 0
FreeAPS/Sources/Router/Screen.swift

@@ -24,6 +24,7 @@ enum Screen: Identifiable {
     case manualTempBasal
     case autotuneConfig
     case dataTable
+    case cgm
 
     var id: Int { String(reflecting: self).hashValue }
 }
@@ -75,6 +76,8 @@ extension Screen {
             return AutotuneConfig.Builder(resolver: resolver).buildView()
         case .dataTable:
             return DataTable.Builder(resolver: resolver).buildView()
+        case .cgm:
+            return CGM.Builder(resolver: resolver).buildView()
         }
     }
 

+ 4 - 0
FreeAPS/Sources/Services/Network/NightscoutManager.swift

@@ -62,6 +62,10 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     }
 
     var cgmURL: URL? {
+        if settingsManager.settings.cgm == .xdrip {
+            return URL(string: "xdripswift://")!
+        }
+
         let useLocal = (settingsManager.settings.useLocalGlucoseSource ?? false) && settingsManager.settings
             .localGlucosePort != nil