Quellcode durchsuchen

Manual glucose updates (#261)

* Display manual glucose entries in iAPS similar to Nightscout and xDrip.
* Upload to NIghtscout when created.
* Delete from Nightcsout when deleted in iAPS.
* Change colour of FPUs to work with the Nightscout format of manual glucose entries.
* Remove amounts of carb equivalents in Chart to make view less busy. This is a temporary fix until removed entirely when we have Fat and Protein models instead. 
* Make pop-up for new glucose cleaner and more prominent. Using both layers and shadow. This is a work in progress and in the future the oref0 info alerts will be replaced with same pop-ups, with images and charts etc. 
* Make glucose data table cleaner. Remove IDs and add when manual instead.
Jon B Mårtensson vor 2 Jahren
Ursprung
Commit
cb6e1fcc4b

+ 1 - 0
FreeAPS/Sources/APS/OpenAPS/Constants.swift

@@ -87,6 +87,7 @@ extension OpenAPS {
         static let uploadedProfile = "upload/uploaded-profile.json"
         static let uploadedPreferences = "upload/uploaded-preferences.json"
         static let uploadedSettings = "upload/uploaded-settings.json"
+        static let uploadedManualGlucose = "upload/uploaded-manual-readings.json"
     }
 
     enum FreeAPS {

+ 0 - 1
FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift

@@ -267,7 +267,6 @@ final class OpenAPS {
                     uamMinutes: (overrideArray.first?.uamMinutes ?? uamMinutes) as Decimal
                 )
                 storage.save(averages, as: OpenAPS.Monitor.oref2_variables)
-                print("Test time for oref2_variables: \(-now.timeIntervalSinceNow) seconds")
                 return self.loadFileFromStorage(name: Monitor.oref2_variables)
 
             } else {

+ 32 - 4
FreeAPS/Sources/APS/Storage/GlucoseStorage.swift

@@ -16,6 +16,7 @@ protocol GlucoseStorage {
     func isGlucoseNotFlat() -> Bool
     func nightscoutGlucoseNotUploaded() -> [BloodGlucose]
     func nightscoutCGMStateNotUploaded() -> [NigtscoutTreatment]
+    func nightscoutManualGlucoseNotUploaded() -> [NigtscoutTreatment]
     var alarm: GlucoseAlarm? { get }
 }
 
@@ -35,9 +36,18 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
         injectServices(resolver)
     }
 
-    func storeGlucose(_ glucose: [BloodGlucose]) {
-        let storeGlucoseStarted = Date()
+    private var glucoseFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 0
+        if settingsManager.settings.units == .mmolL {
+            formatter.maximumFractionDigits = 1
+        }
+        formatter.decimalSeparator = "."
+        return formatter
+    }
 
+    func storeGlucose(_ glucose: [BloodGlucose]) {
         processQueue.sync {
             debug(.deviceManager, "start storage glucose")
             let file = OpenAPS.Monitor.glucose
@@ -136,7 +146,6 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                 }
             }
         }
-        print("Test time of glucoseStorage: \(-1 * storeGlucoseStarted.timeIntervalSinceNow) s")
     }
 
     func removeGlucose(ids: [String]) {
@@ -212,10 +221,29 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
     func nightscoutCGMStateNotUploaded() -> [NigtscoutTreatment] {
         let uploaded = storage.retrieve(OpenAPS.Nightscout.uploadedCGMState, as: [NigtscoutTreatment].self) ?? []
         let recent = storage.retrieve(OpenAPS.Monitor.cgmState, as: [NigtscoutTreatment].self) ?? []
-
         return Array(Set(recent).subtracting(Set(uploaded)))
     }
 
+    func nightscoutManualGlucoseNotUploaded() -> [NigtscoutTreatment] {
+        let uploaded = (storage.retrieve(OpenAPS.Nightscout.uploadedGlucose, as: [BloodGlucose].self) ?? [])
+            .filter({ $0.type == GlucoseType.manual.rawValue })
+        let recent = recent().filter({ $0.type == GlucoseType.manual.rawValue })
+        let filtered = Array(Set(recent).subtracting(Set(uploaded)))
+        let manualReadings = filtered.map { item -> NigtscoutTreatment in
+            NigtscoutTreatment(
+                duration: nil, rawDuration: nil, rawRate: nil, absolute: nil, rate: nil, eventType: .capillaryGlucose,
+                createdAt: item.dateString, enteredBy: "iAPS", bolus: nil, insulin: nil, notes: "iAPS User", carbs: nil,
+                fat: nil,
+                protein: nil, foodType: nil, targetTop: nil, targetBottom: nil, glucoseType: "Manual",
+                glucose: settingsManager.settings
+                    .units == .mgdL ? (glucoseFormatter.string(from: Int(item.glucose ?? 100) as NSNumber) ?? "")
+                    : (glucoseFormatter.string(from: Decimal(item.glucose ?? 100).asMmolL as NSNumber) ?? ""),
+                units: settingsManager.settings.units == .mmolL ? "mmol" : "mg/dl"
+            )
+        }
+        return manualReadings
+    }
+
     var alarm: GlucoseAlarm? {
         guard let glucose = recent().last, glucose.dateString.addingTimeInterval(20.minutes.timeInterval) > Date(),
               let glucoseValue = glucose.glucose else { return nil }

+ 0 - 2
FreeAPS/Sources/Models/BloodGlucose.swift

@@ -29,9 +29,7 @@ struct BloodGlucose: JSON, Identifiable, Hashable {
     let filtered: Decimal?
     let noise: Int?
     var glucose: Int?
-
     let type: String?
-
     var activationDate: Date? = nil
     var sessionStartDate: Date? = nil
     var transmitterID: String? = nil

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

@@ -13,6 +13,7 @@ struct Glucose: JSON {
 enum GlucoseType: String, JSON {
     case sgv
     case cal
+    case manual = "Manual"
 }
 
 enum Direction: String, JSON {

+ 6 - 0
FreeAPS/Sources/Models/NightscoutTreatment.swift

@@ -18,6 +18,9 @@ struct NigtscoutTreatment: JSON, Hashable, Equatable {
     var foodType: String?
     let targetTop: Decimal?
     let targetBottom: Decimal?
+    var glucoseType: String?
+    var glucose: String?
+    var units: String?
 
     static let local = "iAPS"
 
@@ -51,5 +54,8 @@ extension NigtscoutTreatment {
         case foodType
         case targetTop
         case targetBottom
+        case glucoseType
+        case glucose
+        case units
     }
 }

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

@@ -65,6 +65,7 @@ enum EventType: String, JSON {
     case nsBatteryChange = "Pump Battery Change"
     case nsAnnouncement = "Announcement"
     case nsSensorChange = "Sensor Start"
+    case capillaryGlucose = "BG Check"
 }
 
 enum TempType: String, JSON {

+ 1 - 1
FreeAPS/Sources/Modules/DataTable/DataTableDataFlow.swift

@@ -170,7 +170,7 @@ enum DataTable {
             case .carbs:
                 return .loopYellow
             case .fpus:
-                return .loopRed
+                return .orange.opacity(0.5)
             case .bolus:
                 return .insulin
             case .tempBasal:

+ 4 - 0
FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift

@@ -49,5 +49,9 @@ extension DataTable {
             glucoseStorage.removeGlucose(ids: [id])
             healthkitManager.deleteGlucose(syncID: id)
         }
+
+        func deleteManualGlucose(date: Date?) {
+            nightscoutManager.deleteManualGlucose(at: date ?? .distantPast)
+        }
     }
 }

+ 6 - 5
FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -147,7 +147,7 @@ extension DataTable {
         func deleteGlucose(at index: Int) {
             let id = glucose[index].id
             provider.deleteGlucose(id: id)
-
+            // CoreData
             let fetchRequest: NSFetchRequest<NSFetchRequestResult>
             fetchRequest = NSFetchRequest(entityName: "Readings")
             fetchRequest.predicate = NSPredicate(format: "id == %@", id)
@@ -163,10 +163,11 @@ extension DataTable {
                         into: [coredataContext]
                     )
                 }
-            } catch {
-                // To do: handle any thrown errors.
+            } catch { /* To do: handle any thrown errors. */ }
+            // Manual Glucose
+            if (glucose[index].glucose.type ?? "") == GlucoseType.manual.rawValue {
+                provider.deleteManualGlucose(date: glucose[index].glucose.dateString)
             }
-            // try? coredataContext.save()
         }
 
         func addManualGlucose() {
@@ -183,7 +184,7 @@ extension DataTable {
                 filtered: nil,
                 noise: nil,
                 glucose: Int(glucose),
-                type: "Manual"
+                type: GlucoseType.manual.rawValue
             )
             provider.glucoseStorage.storeGlucose([saveToJSON])
             debug(.default, "Manual Glucose saved to glucose.json")

+ 62 - 38
FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift

@@ -12,6 +12,8 @@ extension DataTable {
         @State private var isRemoveInsulinAlertPresented = false
         @State private var removeInsulinAlert: Alert?
         @State private var newGlucose = false
+        @State private var isLayered = false
+        @FocusState private var isFocused: Bool
 
         @Environment(\.colorScheme) var colorScheme
 
@@ -51,40 +53,12 @@ extension DataTable {
                 }
             }
             .onAppear(perform: configureView)
-            .navigationTitle("History")
+            .navigationTitle(isLayered ? "" : "History")
+            .blur(radius: isLayered ? 3.0 : 0)
             .navigationBarTitleDisplayMode(.automatic)
-            .navigationBarItems(
-                leading: Button("Close", action: state.hideModal),
-                trailing: state.mode == .glucose ? EditButton().asAny() : EmptyView().asAny()
-            )
-            .popup(isPresented: newGlucose, alignment: .top, direction: .bottom) {
-                VStack(spacing: 20) {
-                    HStack {
-                        Text("New Glucose")
-                        DecimalTextField(" ... ", value: $state.manualGlcuose, formatter: glucoseFormatter)
-                        Text(state.units.rawValue)
-                    }.padding(.horizontal, 20)
-                    HStack {
-                        let limitLow: Decimal = state.units == .mmolL ? 2.2 : 40
-                        let limitHigh: Decimal = state.units == .mmolL ? 21 : 380
-                        Button { newGlucose = false }
-                        label: { Text("Cancel") }.frame(maxWidth: .infinity, alignment: .leading)
-
-                        Button {
-                            state.addManualGlucose()
-                            newGlucose = false
-                        }
-                        label: { Text("Save") }
-                            .frame(maxWidth: .infinity, alignment: .trailing)
-                        // .disabled(state.manualGlcuose < limitLow || state.manualGlcuose > limitHigh)
-
-                    }.padding(20)
-                }
-                .frame(maxHeight: 140)
-                .background(
-                    RoundedRectangle(cornerRadius: 8, style: .continuous)
-                        .fill(Color(colorScheme == .dark ? UIColor.systemGray2 : UIColor.systemGray6))
-                )
+            .navigationBarItems(leading: Button(isLayered ? "" : "Close", action: state.hideModal))
+            .popup(isPresented: newGlucose, alignment: .center, direction: .top) {
+                addGlucose
             }
         }
 
@@ -98,16 +72,63 @@ extension DataTable {
 
         private var glucoseList: some View {
             List {
-                Button { newGlucose = true }
+                Button {
+                    newGlucose = true
+                    isFocused = true
+                    isLayered.toggle()
+                }
                 label: { Text("Add") }.frame(maxWidth: .infinity, alignment: .trailing)
                     .padding(.trailing, 20)
 
                 ForEach(state.glucose) { item in
-                    glucoseView(item)
+                    glucoseView(item, isManual: item.glucose)
                 }.onDelete(perform: deleteGlucose)
             }
         }
 
+        private var addGlucose: some View {
+            VStack {
+                Form {
+                    Section {
+                        HStack {
+                            Text("Glucose").font(.custom("popup", fixedSize: 18))
+                            DecimalTextField(" ... ", value: $state.manualGlcuose, formatter: glucoseFormatter)
+                                .focused($isFocused).font(.custom("glucose", fixedSize: 22))
+                            Text(state.units.rawValue).foregroundStyle(.secondary)
+                        }
+                    }
+                    header: {
+                        Text("Blood Glucose Test").foregroundColor(.secondary).font(.custom("popupHeader", fixedSize: 12))
+                            .padding(.top)
+                    }
+                    HStack {
+                        Button {
+                            newGlucose = false
+                            isLayered = false
+                        }
+                        label: { Text("Cancel").foregroundColor(.red) }
+                            .frame(maxWidth: .infinity, alignment: .leading)
+                        Spacer()
+                        Button {
+                            state.addManualGlucose()
+                            newGlucose = false
+                            isLayered = false
+                        }
+                        label: { Text("Save") }
+                            .frame(maxWidth: .infinity, alignment: .trailing)
+                            .disabled(state.manualGlcuose <= 0)
+                    }
+                    .buttonStyle(BorderlessButtonStyle())
+                    .font(.custom("popupButtons", fixedSize: 16))
+                }
+            }
+            .frame(maxHeight: 220)
+            .background(
+                RoundedRectangle(cornerRadius: 8, style: .continuous)
+                    .fill(Color(.tertiarySystemBackground))
+            ).border(.gray).shadow(radius: 40)
+        }
+
         @ViewBuilder private func treatmentView(_ item: Treatment) -> some View {
             HStack {
                 Image(systemName: "circle.fill").foregroundColor(item.color)
@@ -192,7 +213,7 @@ extension DataTable {
             }
         }
 
-        @ViewBuilder private func glucoseView(_ item: Glucose) -> some View {
+        @ViewBuilder private func glucoseView(_ item: Glucose, isManual: BloodGlucose) -> some View {
             VStack(alignment: .leading, spacing: 4) {
                 HStack {
                     Text(dateFormatter.string(from: item.glucose.dateString))
@@ -203,9 +224,12 @@ extension DataTable {
                         ) as NSNumber)!
                     } ?? "--")
                     Text(state.units.rawValue)
-                    Text(item.glucose.direction?.symbol ?? "--")
+                    if isManual.type == GlucoseType.manual.rawValue {
+                        Image(systemName: "drop.fill").symbolRenderingMode(.monochrome).foregroundStyle(.red)
+                    } else {
+                        Text(item.glucose.direction?.symbol ?? "--")
+                    }
                 }
-                Text("ID: " + item.glucose.id).font(.caption2).foregroundColor(.secondary)
             }
         }
 

+ 7 - 0
FreeAPS/Sources/Modules/Home/HomeProvider.swift

@@ -28,6 +28,13 @@ extension Home {
             }
         }
 
+        func manualGlucose(hours: Int) -> [BloodGlucose] {
+            glucoseStorage.recent().filter {
+                $0.type == GlucoseType.manual.rawValue &&
+                    $0.dateString.addingTimeInterval(hours.hours.timeInterval) > Date()
+            }
+        }
+
         func pumpHistory(hours: Int) -> [PumpHistoryEvent] {
             pumpHistoryStorage.recent().filter {
                 $0.timestamp.addingTimeInterval(hours.hours.timeInterval) > Date()

+ 2 - 0
FreeAPS/Sources/Modules/Home/HomeStateModel.swift

@@ -12,6 +12,7 @@ extension Home {
         private let timer = DispatchTimer(timeInterval: 5)
         private(set) var filteredHours = 24
         @Published var glucose: [BloodGlucose] = []
+        @Published var isManual: [BloodGlucose] = []
         @Published var suggestion: Suggestion?
         @Published var uploadStats = false
         @Published var enactedSuggestion: Suggestion?
@@ -216,6 +217,7 @@ extension Home {
         private func setupGlucose() {
             DispatchQueue.main.async { [weak self] in
                 guard let self = self else { return }
+                self.isManual = self.provider.manualGlucose(hours: self.filteredHours)
                 self.glucose = self.provider.filteredGlucose(hours: self.filteredHours)
                 self.recentGlucose = self.glucose.last
                 if self.glucose.count >= 2 {

+ 84 - 10
FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift

@@ -30,10 +30,13 @@ struct MainChartView: View {
         static let bolusSize: CGFloat = 8
         static let bolusScale: CGFloat = 2.5
         static let carbsSize: CGFloat = 10
+        static let fpuSize: CGFloat = 5
         static let carbsScale: CGFloat = 0.3
+        static let fpuScale: CGFloat = 1
     }
 
     @Binding var glucose: [BloodGlucose]
+    @Binding var isManual: [BloodGlucose]
     @Binding var suggestion: Suggestion?
     @Binding var tempBasals: [PumpHistoryEvent]
     @Binding var boluses: [PumpHistoryEvent]
@@ -56,6 +59,8 @@ struct MainChartView: View {
 
     @State var didAppearTrigger = false
     @State private var glucoseDots: [CGRect] = []
+    @State private var manualGlucoseDots: [CGRect] = []
+    @State private var manualGlucoseDotsCenter: [CGRect] = []
     @State private var unSmoothedGlucoseDots: [CGRect] = []
     @State private var predictionDots: [PredictionType: [CGRect]] = [:]
     @State private var bolusDots: [DotInfo] = []
@@ -270,6 +275,8 @@ struct MainChartView: View {
                     bolusView(fullSize: fullSize)
                     if smooth { unSmoothedGlucoseView(fullSize: fullSize) }
                     glucoseView(fullSize: fullSize)
+                    manualGlucoseView(fullSize: fullSize)
+                    manualGlucoseCenterView(fullSize: fullSize)
                     predictionsView(fullSize: fullSize)
                 }
                 timeLabelsView(fullSize: fullSize)
@@ -342,6 +349,46 @@ struct MainChartView: View {
         }
     }
 
+    private func manualGlucoseView(fullSize: CGSize) -> some View {
+        Path { path in
+            for rect in manualGlucoseDots {
+                path.addEllipse(in: rect)
+            }
+        }
+        .fill(Color.gray)
+        .onChange(of: isManual) { _ in
+            update(fullSize: fullSize)
+        }
+        .onChange(of: didAppearTrigger) { _ in
+            update(fullSize: fullSize)
+        }
+        .onReceive(Foundation.NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
+            update(fullSize: fullSize)
+        }
+    }
+
+    private func manualGlucoseCenterView(fullSize: CGSize) -> some View {
+        Path { path in
+            for rect in manualGlucoseDotsCenter {
+                path.addEllipse(in: rect)
+            }
+        }
+        .fill(Color.red)
+
+        .onChange(of: isManual) { _ in
+            update(fullSize: fullSize)
+        }
+        .onChange(of: didAppearTrigger) { _ in
+            update(fullSize: fullSize)
+        }
+        .onReceive(
+            Foundation.NotificationCenter.default
+                .publisher(for: UIApplication.willEnterForegroundNotification)
+        ) { _ in
+            update(fullSize: fullSize)
+        }
+    }
+
     private func unSmoothedGlucoseView(fullSize: CGSize) -> some View {
         Path { path in
             var lines: [CGPoint] = []
@@ -410,16 +457,9 @@ struct MainChartView: View {
     private func fpuView(fullSize: CGSize) -> some View {
         ZStack {
             fpuPath
-                .fill(Color.red)
+                .fill(.orange.opacity(0.5))
             fpuPath
-                .stroke(Color.primary, lineWidth: 0.5)
-
-            ForEach(fpuDots, id: \.rect.minX) { info -> AnyView in
-                let position = CGPoint(x: info.rect.midX, y: info.rect.minY - 8)
-                return Text(fpuFormatter.string(from: info.value as NSNumber)!).font(.caption2)
-                    .position(position)
-                    .asAny()
-            }
+                .stroke(Color.primary, lineWidth: 0.2)
         }
         .onChange(of: carbs) { _ in
             calculateFPUsDots(fullSize: fullSize)
@@ -488,6 +528,8 @@ extension MainChartView {
         calculatePredictionDots(fullSize: fullSize, type: .zt)
         calculatePredictionDots(fullSize: fullSize, type: .uam)
         calculateGlucoseDots(fullSize: fullSize)
+        calculateManualGlucoseDots(fullSize: fullSize)
+        calculateManualGlucoseDotsCenter(fullSize: fullSize)
         calculateUnSmoothedGlucoseDots(fullSize: fullSize)
         calculateBolusDots(fullSize: fullSize)
         calculateCarbsDots(fullSize: fullSize)
@@ -513,6 +555,38 @@ extension MainChartView {
         }
     }
 
+    private func calculateManualGlucoseDots(fullSize: CGSize) {
+        calculationQueue.async {
+            let dots = isManual.concurrentMap { value -> CGRect in
+                let position = glucoseToCoordinate(value, fullSize: fullSize)
+                return CGRect(x: position.x - 2, y: position.y - 2, width: 14, height: 14)
+            }
+
+            let range = self.getGlucoseYRange(fullSize: fullSize)
+
+            DispatchQueue.main.async {
+                glucoseYRange = range
+                manualGlucoseDots = dots
+            }
+        }
+    }
+
+    private func calculateManualGlucoseDotsCenter(fullSize: CGSize) {
+        calculationQueue.async {
+            let dots = isManual.concurrentMap { value -> CGRect in
+                let position = glucoseToCoordinate(value, fullSize: fullSize)
+                return CGRect(x: position.x, y: position.y, width: 10, height: 10)
+            }
+
+            let range = self.getGlucoseYRange(fullSize: fullSize)
+
+            DispatchQueue.main.async {
+                glucoseYRange = range
+                manualGlucoseDotsCenter = dots
+            }
+        }
+    }
+
     private func calculateUnSmoothedGlucoseDots(fullSize: CGSize) {
         calculationQueue.async {
             let dots = glucose.concurrentMap { value -> CGRect in
@@ -579,7 +653,7 @@ extension MainChartView {
             let fpus = carbs.filter { $0.isFPU ?? false }
             let dots = fpus.map { value -> DotInfo in
                 let center = timeToInterpolatedPoint(value.createdAt.timeIntervalSince1970, fullSize: fullSize)
-                let size = Config.carbsSize + CGFloat(value.carbs) * Config.carbsScale
+                let size = Config.fpuSize + CGFloat(value.carbs) * Config.fpuScale
                 let rect = CGRect(x: center.x - size / 2, y: center.y - size / 2, width: size, height: size)
                 return DotInfo(rect: rect, value: value.carbs)
             }

+ 1 - 0
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -384,6 +384,7 @@ extension Home {
 
                 MainChartView(
                     glucose: $state.glucose,
+                    isManual: $state.isManual,
                     suggestion: $state.suggestion,
                     tempBasals: $state.tempBasals,
                     boluses: $state.boluses,

+ 53 - 0
FreeAPS/Sources/Services/Network/NightscoutAPI.swift

@@ -170,6 +170,35 @@ extension NightscoutAPI {
             .eraseToAnyPublisher()
     }
 
+    func deleteManualGlucose(at date: Date) -> AnyPublisher<Void, Swift.Error> {
+        var components = URLComponents()
+        components.scheme = url.scheme
+        components.host = url.host
+        components.port = url.port
+        components.path = Config.treatmentsPath
+        components.queryItems = [
+            URLQueryItem(name: "find[glucose][$exists]", value: "true"),
+            URLQueryItem(
+                name: "find[created_at][$eq]",
+                value: Formatter.iso8601withFractionalSeconds.string(from: date)
+            )
+        ]
+
+        var request = URLRequest(url: components.url!)
+        request.allowsConstrainedNetworkAccess = false
+        request.timeoutInterval = Config.timeout
+        request.httpMethod = "DELETE"
+
+        if let secret = secret {
+            request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
+        }
+
+        return service.run(request)
+            .retry(Config.retryCount)
+            .map { _ in () }
+            .eraseToAnyPublisher()
+    }
+
     func deleteInsulin(at date: Date) -> AnyPublisher<Void, Swift.Error> {
         var components = URLComponents()
         components.scheme = url.scheme
@@ -445,6 +474,30 @@ extension NightscoutAPI {
             .map { _ in () }
             .eraseToAnyPublisher()
     }
+
+    func uploadPreferences(_ preferences: Preferences) -> AnyPublisher<Void, Swift.Error> {
+        var components = URLComponents()
+        components.scheme = url.scheme
+        components.host = url.host
+        components.port = url.port
+        components.path = Config.profilePath
+
+        var request = URLRequest(url: components.url!)
+        request.allowsConstrainedNetworkAccess = false
+        request.timeoutInterval = Config.timeout
+        request.addValue("application/json", forHTTPHeaderField: "Content-Type")
+
+        if let secret = secret {
+            request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
+        }
+        request.httpBody = try! JSONCoding.encoder.encode(preferences)
+        request.httpMethod = "POST"
+
+        return service.run(request)
+            .retry(Config.retryCount)
+            .map { _ in () }
+            .eraseToAnyPublisher()
+    }
 }
 
 private extension String {

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

@@ -11,8 +11,10 @@ protocol NightscoutManager: GlucoseSource {
     func fetchAnnouncements() -> AnyPublisher<[Announcement], Never>
     func deleteCarbs(at date: Date, isFPU: Bool?, fpuID: String?, syncID: String)
     func deleteInsulin(at date: Date)
+    func deleteManualGlucose(at: Date)
     func uploadStatus()
     func uploadGlucose()
+    func uploadManualGlucose()
     func uploadStatistics(dailystat: Statistics)
     func uploadPreferences(_ preferences: Preferences)
     func uploadProfileAndSettings()
@@ -68,6 +70,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         broadcaster.register(PumpHistoryObserver.self, observer: self)
         broadcaster.register(CarbsObserver.self, observer: self)
         broadcaster.register(TempTargetsObserver.self, observer: self)
+        broadcaster.register(GlucoseObserver.self, observer: self)
         _ = reachabilityManager.startListening(onQueue: processQueue) { status in
             debug(.nightscout, "Network status: \(status)")
         }
@@ -251,6 +254,22 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             .store(in: &lifetime)
     }
 
+    func deleteManualGlucose(at date: Date) {
+        guard let nightscout = nightscoutAPI, isUploadEnabled else {
+            return
+        }
+        nightscout.deleteManualGlucose(at: date)
+            .sink { completion in
+                switch completion {
+                case .finished:
+                    debug(.nightscout, "Manual Glucose entry deleted")
+                case let .failure(error):
+                    debug(.nightscout, error.localizedDescription)
+                }
+            } receiveValue: {}
+            .store(in: &lifetime)
+    }
+
     func uploadStatistics(dailystat: Statistics) {
         let stats = NightscoutStatistics(
             dailystats: dailystat
@@ -568,6 +587,13 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         uploadTreatments(glucoseStorage.nightscoutCGMStateNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedCGMState)
     }
 
+    func uploadManualGlucose() {
+        uploadTreatments(
+            glucoseStorage.nightscoutManualGlucoseNotUploaded(),
+            fileToSave: OpenAPS.Nightscout.uploadedManualGlucose
+        )
+    }
+
     private func uploadPumpHistory() {
         uploadTreatments(pumpHistoryStorage.nightscoutTretmentsNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedPumphistory)
     }
@@ -658,3 +684,9 @@ extension BaseNightscoutManager: TempTargetsObserver {
         uploadTempTargets()
     }
 }
+
+extension BaseNightscoutManager: GlucoseObserver {
+    func glucoseDidUpdate(_: [BloodGlucose]) {
+        uploadManualGlucose()
+    }
+}