Sfoglia il codice sorgente

Merge branch 'Crowdin' into crowdin_generated

Jon B Mårtensson 2 anni fa
parent
commit
6e1d4b907b
24 ha cambiato i file con 321 aggiunte e 76 eliminazioni
  1. 4 4
      Dependencies/G7SensorKit/G7SensorKitUI/Views/G7SettingsView.swift
  2. 1 1
      FreeAPS/Resources/javascript/bundle/determine-basal.js
  3. 7 4
      FreeAPS/Sources/APS/APSManager.swift
  4. 1 0
      FreeAPS/Sources/APS/OpenAPS/Constants.swift
  5. 0 1
      FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift
  6. 32 4
      FreeAPS/Sources/APS/Storage/GlucoseStorage.swift
  7. 1 1
      FreeAPS/Sources/Localizations/Main/ca.lproj/Localizable.strings
  8. 4 1
      FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings
  9. 0 2
      FreeAPS/Sources/Models/BloodGlucose.swift
  10. 1 0
      FreeAPS/Sources/Models/Glucose.swift
  11. 6 0
      FreeAPS/Sources/Models/NightscoutTreatment.swift
  12. 1 0
      FreeAPS/Sources/Models/PumpHistoryEvent.swift
  13. 1 1
      FreeAPS/Sources/Modules/DataTable/DataTableDataFlow.swift
  14. 4 0
      FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift
  15. 11 5
      FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift
  16. 63 40
      FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift
  17. 7 0
      FreeAPS/Sources/Modules/Home/HomeProvider.swift
  18. 2 0
      FreeAPS/Sources/Modules/Home/HomeStateModel.swift
  19. 84 10
      FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift
  20. 1 1
      FreeAPS/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift
  21. 4 0
      FreeAPS/Sources/Modules/Home/View/HomeRootView.swift
  22. 53 0
      FreeAPS/Sources/Services/Network/NightscoutAPI.swift
  23. 32 0
      FreeAPS/Sources/Services/Network/NightscoutManager.swift
  24. 1 1
      FreeAPS/Sources/Services/UserNotifiactions/UserNotificationsManager.swift

+ 4 - 4
Dependencies/G7SensorKit/G7SensorKitUI/Views/G7SettingsView.swift

@@ -72,7 +72,7 @@ struct G7SettingsView: View {
                 }
                 }
             }
             }
 
 
-            Section("Last Reading") {
+            Section(LocalizedString("Last Reading", comment: "")) {
                 LabeledValueView(label: LocalizedString("Glucose", comment: "Field label"),
                 LabeledValueView(label: LocalizedString("Glucose", comment: "Field label"),
                                  value: viewModel.lastGlucoseString)
                                  value: viewModel.lastGlucoseString)
                 LabeledDateView(label: LocalizedString("Time", comment: "Field label"),
                 LabeledDateView(label: LocalizedString("Time", comment: "Field label"),
@@ -82,7 +82,7 @@ struct G7SettingsView: View {
                                  value: viewModel.lastGlucoseTrendString)
                                  value: viewModel.lastGlucoseTrendString)
             }
             }
 
 
-            Section("Bluetooth") {
+            Section(LocalizedString("Bluetooth", comment: "")) {
                 if let name = viewModel.sensorName {
                 if let name = viewModel.sensorName {
                     HStack {
                     HStack {
                         Text(LocalizedString("Name", comment: "title for g7 settings row showing BLE Name"))
                         Text(LocalizedString("Name", comment: "title for g7 settings row showing BLE Name"))
@@ -114,7 +114,7 @@ struct G7SettingsView: View {
                 }
                 }
             }
             }
 
 
-            Section("Configuration") {
+            Section(LocalizedString("Configuration", comment: "")) {
                 HStack {
                 HStack {
                     Toggle(LocalizedString("Upload Readings", comment: "title for g7 config settings to upload readings"), isOn: $viewModel.uploadReadings)
                     Toggle(LocalizedString("Upload Readings", comment: "title for g7 config settings to upload readings"), isOn: $viewModel.uploadReadings)
                 }
                 }
@@ -122,7 +122,7 @@ struct G7SettingsView: View {
 
 
             Section () {
             Section () {
                 if !self.viewModel.scanning {
                 if !self.viewModel.scanning {
-                    Button("Scan for new sensor", action: {
+                    Button(LocalizedString("Scan for new sensor", comment: ""), action: {
                         self.viewModel.scanForNewSensor()
                         self.viewModel.scanForNewSensor()
                     })
                     })
                 }
                 }

File diff suppressed because it is too large
+ 1 - 1
FreeAPS/Resources/javascript/bundle/determine-basal.js


+ 7 - 4
FreeAPS/Sources/APS/APSManager.swift

@@ -342,10 +342,13 @@ final class BaseAPSManager: APSManager, Injectable {
             return Just(false).eraseToAnyPublisher()
             return Just(false).eraseToAnyPublisher()
         }
         }
 
 
-        guard glucoseStorage.isGlucoseNotFlat() else {
-            debug(.apsManager, "Glucose data is too flat")
-            processError(APSError.glucoseError(message: "Glucose data is too flat"))
-            return Just(false).eraseToAnyPublisher()
+        // Only let glucose be flat when 400 mg/dl
+        if (glucoseStorage.recent().last?.glucose ?? 100) != 400 {
+            guard glucoseStorage.isGlucoseNotFlat() else {
+                debug(.apsManager, "Glucose data is too flat")
+                processError(APSError.glucoseError(message: "Glucose data is too flat"))
+                return Just(false).eraseToAnyPublisher()
+            }
         }
         }
 
 
         let now = Date()
         let now = Date()

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

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

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

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

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

@@ -16,6 +16,7 @@ protocol GlucoseStorage {
     func isGlucoseNotFlat() -> Bool
     func isGlucoseNotFlat() -> Bool
     func nightscoutGlucoseNotUploaded() -> [BloodGlucose]
     func nightscoutGlucoseNotUploaded() -> [BloodGlucose]
     func nightscoutCGMStateNotUploaded() -> [NigtscoutTreatment]
     func nightscoutCGMStateNotUploaded() -> [NigtscoutTreatment]
+    func nightscoutManualGlucoseNotUploaded() -> [NigtscoutTreatment]
     var alarm: GlucoseAlarm? { get }
     var alarm: GlucoseAlarm? { get }
 }
 }
 
 
@@ -35,9 +36,18 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
         injectServices(resolver)
         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 {
         processQueue.sync {
             debug(.deviceManager, "start storage glucose")
             debug(.deviceManager, "start storage glucose")
             let file = OpenAPS.Monitor.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]) {
     func removeGlucose(ids: [String]) {
@@ -212,10 +221,29 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
     func nightscoutCGMStateNotUploaded() -> [NigtscoutTreatment] {
     func nightscoutCGMStateNotUploaded() -> [NigtscoutTreatment] {
         let uploaded = storage.retrieve(OpenAPS.Nightscout.uploadedCGMState, as: [NigtscoutTreatment].self) ?? []
         let uploaded = storage.retrieve(OpenAPS.Nightscout.uploadedCGMState, as: [NigtscoutTreatment].self) ?? []
         let recent = storage.retrieve(OpenAPS.Monitor.cgmState, as: [NigtscoutTreatment].self) ?? []
         let recent = storage.retrieve(OpenAPS.Monitor.cgmState, as: [NigtscoutTreatment].self) ?? []
-
         return Array(Set(recent).subtracting(Set(uploaded)))
         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? {
     var alarm: GlucoseAlarm? {
         guard let glucose = recent().last, glucose.dateString.addingTimeInterval(20.minutes.timeInterval) > Date(),
         guard let glucose = recent().last, glucose.dateString.addingTimeInterval(20.minutes.timeInterval) > Date(),
               let glucoseValue = glucose.glucose else { return nil }
               let glucoseValue = glucose.glucose else { return nil }

+ 1 - 1
FreeAPS/Sources/Localizations/Main/ca.lproj/Localizable.strings

@@ -940,7 +940,7 @@ Enact a temp Basal or a temp target */
 "iAPS not active" = "iAPS not active";
 "iAPS not active" = "iAPS not active";
 
 
 /* */
 /* */
-"Last loop was more then %d min ago" = "Last loop was more then %d min ago";
+"Last loop was more than %d min ago" = "Last loop was more than %d min ago";
 
 
 /* Glucose badge */
 /* Glucose badge */
 "Show glucose on the app badge" = "Show glucose on the app badge";
 "Show glucose on the app badge" = "Show glucose on the app badge";

+ 4 - 1
FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings

@@ -350,6 +350,9 @@ Enact a temp Basal or a temp target */
 /* Import Error */
 /* Import Error */
 "Can't find the default Nightscout Profile." = "Can't find the default Nightscout Profile.";
 "Can't find the default Nightscout Profile." = "Can't find the default Nightscout Profile.";
 
 
+/* Add Blood Glucose Test, header */
+"Blood Glucose Test" = "Blood Glucose Test";
+
 /* Add Medtronic pump */
 /* Add Medtronic pump */
 "Add Medtronic" = "Add Medtronic";
 "Add Medtronic" = "Add Medtronic";
 
 
@@ -987,7 +990,7 @@ Enact a temp Basal or a temp target */
  "iAPS not active" = "iAPS not active";
  "iAPS not active" = "iAPS not active";
 
 
  /* */
  /* */
- "Last loop was more then %d min ago" = "Last loop was more then %d min ago";
+ "Last loop was more than %d min ago" = "Last loop was more than %d min ago";
 
 
 /* Glucose badge */
 /* Glucose badge */
 "Show glucose on the app badge" = "Show glucose on the app badge";
 "Show glucose on the app badge" = "Show glucose on the app badge";

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

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

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

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

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

@@ -18,6 +18,9 @@ struct NigtscoutTreatment: JSON, Hashable, Equatable {
     var foodType: String?
     var foodType: String?
     let targetTop: Decimal?
     let targetTop: Decimal?
     let targetBottom: Decimal?
     let targetBottom: Decimal?
+    var glucoseType: String?
+    var glucose: String?
+    var units: String?
 
 
     static let local = "iAPS"
     static let local = "iAPS"
 
 
@@ -51,5 +54,8 @@ extension NigtscoutTreatment {
         case foodType
         case foodType
         case targetTop
         case targetTop
         case targetBottom
         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 nsBatteryChange = "Pump Battery Change"
     case nsAnnouncement = "Announcement"
     case nsAnnouncement = "Announcement"
     case nsSensorChange = "Sensor Start"
     case nsSensorChange = "Sensor Start"
+    case capillaryGlucose = "BG Check"
 }
 }
 
 
 enum TempType: String, JSON {
 enum TempType: String, JSON {

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

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

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

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

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

@@ -6,6 +6,7 @@ extension DataTable {
         @Injected() var broadcaster: Broadcaster!
         @Injected() var broadcaster: Broadcaster!
         @Injected() var unlockmanager: UnlockManager!
         @Injected() var unlockmanager: UnlockManager!
         @Injected() private var storage: FileStorage!
         @Injected() private var storage: FileStorage!
+        @Injected() var healthKitManager: HealthKitManager!
 
 
         let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
         let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
 
 
@@ -147,7 +148,7 @@ extension DataTable {
         func deleteGlucose(at index: Int) {
         func deleteGlucose(at index: Int) {
             let id = glucose[index].id
             let id = glucose[index].id
             provider.deleteGlucose(id: id)
             provider.deleteGlucose(id: id)
-
+            // CoreData
             let fetchRequest: NSFetchRequest<NSFetchRequestResult>
             let fetchRequest: NSFetchRequest<NSFetchRequestResult>
             fetchRequest = NSFetchRequest(entityName: "Readings")
             fetchRequest = NSFetchRequest(entityName: "Readings")
             fetchRequest.predicate = NSPredicate(format: "id == %@", id)
             fetchRequest.predicate = NSPredicate(format: "id == %@", id)
@@ -163,10 +164,11 @@ extension DataTable {
                         into: [coredataContext]
                         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() {
         func addManualGlucose() {
@@ -183,10 +185,14 @@ extension DataTable {
                 filtered: nil,
                 filtered: nil,
                 noise: nil,
                 noise: nil,
                 glucose: Int(glucose),
                 glucose: Int(glucose),
-                type: "Manual"
+                type: GlucoseType.manual.rawValue
             )
             )
             provider.glucoseStorage.storeGlucose([saveToJSON])
             provider.glucoseStorage.storeGlucose([saveToJSON])
             debug(.default, "Manual Glucose saved to glucose.json")
             debug(.default, "Manual Glucose saved to glucose.json")
+            // Save to Health
+            var saveToHealth = [BloodGlucose]()
+            saveToHealth.append(saveToJSON)
+            healthKitManager.saveIfNeeded(bloodGlucose: saveToHealth)
         }
         }
     }
     }
 }
 }

+ 63 - 40
FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift

@@ -12,6 +12,8 @@ extension DataTable {
         @State private var isRemoveInsulinAlertPresented = false
         @State private var isRemoveInsulinAlertPresented = false
         @State private var removeInsulinAlert: Alert?
         @State private var removeInsulinAlert: Alert?
         @State private var newGlucose = false
         @State private var newGlucose = false
+        @State private var isLayered = false
+        @FocusState private var isFocused: Bool
 
 
         @Environment(\.colorScheme) var colorScheme
         @Environment(\.colorScheme) var colorScheme
 
 
@@ -20,10 +22,9 @@ extension DataTable {
             formatter.numberStyle = .decimal
             formatter.numberStyle = .decimal
             formatter.maximumFractionDigits = 0
             formatter.maximumFractionDigits = 0
             if state.units == .mmolL {
             if state.units == .mmolL {
-                formatter.minimumFractionDigits = 1
                 formatter.maximumFractionDigits = 1
                 formatter.maximumFractionDigits = 1
+                formatter.roundingMode = .ceiling
             }
             }
-            formatter.roundingMode = .halfUp
             return formatter
             return formatter
         }
         }
 
 
@@ -51,40 +52,12 @@ extension DataTable {
                 }
                 }
             }
             }
             .onAppear(perform: configureView)
             .onAppear(perform: configureView)
-            .navigationTitle("History")
+            .navigationTitle(isLayered ? "" : "History")
+            .blur(radius: isLayered ? 3.0 : 0)
             .navigationBarTitleDisplayMode(.automatic)
             .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 +71,63 @@ extension DataTable {
 
 
         private var glucoseList: some View {
         private var glucoseList: some View {
             List {
             List {
-                Button { newGlucose = true }
+                Button {
+                    newGlucose = true
+                    isFocused = true
+                    isLayered.toggle()
+                }
                 label: { Text("Add") }.frame(maxWidth: .infinity, alignment: .trailing)
                 label: { Text("Add") }.frame(maxWidth: .infinity, alignment: .trailing)
                     .padding(.trailing, 20)
                     .padding(.trailing, 20)
 
 
                 ForEach(state.glucose) { item in
                 ForEach(state.glucose) { item in
-                    glucoseView(item)
+                    glucoseView(item, isManual: item.glucose)
                 }.onDelete(perform: deleteGlucose)
                 }.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 {
         @ViewBuilder private func treatmentView(_ item: Treatment) -> some View {
             HStack {
             HStack {
                 Image(systemName: "circle.fill").foregroundColor(item.color)
                 Image(systemName: "circle.fill").foregroundColor(item.color)
@@ -192,7 +212,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) {
             VStack(alignment: .leading, spacing: 4) {
                 HStack {
                 HStack {
                     Text(dateFormatter.string(from: item.glucose.dateString))
                     Text(dateFormatter.string(from: item.glucose.dateString))
@@ -203,9 +223,12 @@ extension DataTable {
                         ) as NSNumber)!
                         ) as NSNumber)!
                     } ?? "--")
                     } ?? "--")
                     Text(state.units.rawValue)
                     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] {
         func pumpHistory(hours: Int) -> [PumpHistoryEvent] {
             pumpHistoryStorage.recent().filter {
             pumpHistoryStorage.recent().filter {
                 $0.timestamp.addingTimeInterval(hours.hours.timeInterval) > Date()
                 $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 let timer = DispatchTimer(timeInterval: 5)
         private(set) var filteredHours = 24
         private(set) var filteredHours = 24
         @Published var glucose: [BloodGlucose] = []
         @Published var glucose: [BloodGlucose] = []
+        @Published var isManual: [BloodGlucose] = []
         @Published var suggestion: Suggestion?
         @Published var suggestion: Suggestion?
         @Published var uploadStats = false
         @Published var uploadStats = false
         @Published var enactedSuggestion: Suggestion?
         @Published var enactedSuggestion: Suggestion?
@@ -216,6 +217,7 @@ extension Home {
         private func setupGlucose() {
         private func setupGlucose() {
             DispatchQueue.main.async { [weak self] in
             DispatchQueue.main.async { [weak self] in
                 guard let self = self else { return }
                 guard let self = self else { return }
+                self.isManual = self.provider.manualGlucose(hours: self.filteredHours)
                 self.glucose = self.provider.filteredGlucose(hours: self.filteredHours)
                 self.glucose = self.provider.filteredGlucose(hours: self.filteredHours)
                 self.recentGlucose = self.glucose.last
                 self.recentGlucose = self.glucose.last
                 if self.glucose.count >= 2 {
                 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 bolusSize: CGFloat = 8
         static let bolusScale: CGFloat = 2.5
         static let bolusScale: CGFloat = 2.5
         static let carbsSize: CGFloat = 10
         static let carbsSize: CGFloat = 10
+        static let fpuSize: CGFloat = 5
         static let carbsScale: CGFloat = 0.3
         static let carbsScale: CGFloat = 0.3
+        static let fpuScale: CGFloat = 1
     }
     }
 
 
     @Binding var glucose: [BloodGlucose]
     @Binding var glucose: [BloodGlucose]
+    @Binding var isManual: [BloodGlucose]
     @Binding var suggestion: Suggestion?
     @Binding var suggestion: Suggestion?
     @Binding var tempBasals: [PumpHistoryEvent]
     @Binding var tempBasals: [PumpHistoryEvent]
     @Binding var boluses: [PumpHistoryEvent]
     @Binding var boluses: [PumpHistoryEvent]
@@ -56,6 +59,8 @@ struct MainChartView: View {
 
 
     @State var didAppearTrigger = false
     @State var didAppearTrigger = false
     @State private var glucoseDots: [CGRect] = []
     @State private var glucoseDots: [CGRect] = []
+    @State private var manualGlucoseDots: [CGRect] = []
+    @State private var manualGlucoseDotsCenter: [CGRect] = []
     @State private var unSmoothedGlucoseDots: [CGRect] = []
     @State private var unSmoothedGlucoseDots: [CGRect] = []
     @State private var predictionDots: [PredictionType: [CGRect]] = [:]
     @State private var predictionDots: [PredictionType: [CGRect]] = [:]
     @State private var bolusDots: [DotInfo] = []
     @State private var bolusDots: [DotInfo] = []
@@ -270,6 +275,8 @@ struct MainChartView: View {
                     bolusView(fullSize: fullSize)
                     bolusView(fullSize: fullSize)
                     if smooth { unSmoothedGlucoseView(fullSize: fullSize) }
                     if smooth { unSmoothedGlucoseView(fullSize: fullSize) }
                     glucoseView(fullSize: fullSize)
                     glucoseView(fullSize: fullSize)
+                    manualGlucoseView(fullSize: fullSize)
+                    manualGlucoseCenterView(fullSize: fullSize)
                     predictionsView(fullSize: fullSize)
                     predictionsView(fullSize: fullSize)
                 }
                 }
                 timeLabelsView(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 {
     private func unSmoothedGlucoseView(fullSize: CGSize) -> some View {
         Path { path in
         Path { path in
             var lines: [CGPoint] = []
             var lines: [CGPoint] = []
@@ -410,16 +457,9 @@ struct MainChartView: View {
     private func fpuView(fullSize: CGSize) -> some View {
     private func fpuView(fullSize: CGSize) -> some View {
         ZStack {
         ZStack {
             fpuPath
             fpuPath
-                .fill(Color.red)
+                .fill(.orange.opacity(0.5))
             fpuPath
             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
         .onChange(of: carbs) { _ in
             calculateFPUsDots(fullSize: fullSize)
             calculateFPUsDots(fullSize: fullSize)
@@ -488,6 +528,8 @@ extension MainChartView {
         calculatePredictionDots(fullSize: fullSize, type: .zt)
         calculatePredictionDots(fullSize: fullSize, type: .zt)
         calculatePredictionDots(fullSize: fullSize, type: .uam)
         calculatePredictionDots(fullSize: fullSize, type: .uam)
         calculateGlucoseDots(fullSize: fullSize)
         calculateGlucoseDots(fullSize: fullSize)
+        calculateManualGlucoseDots(fullSize: fullSize)
+        calculateManualGlucoseDotsCenter(fullSize: fullSize)
         calculateUnSmoothedGlucoseDots(fullSize: fullSize)
         calculateUnSmoothedGlucoseDots(fullSize: fullSize)
         calculateBolusDots(fullSize: fullSize)
         calculateBolusDots(fullSize: fullSize)
         calculateCarbsDots(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) {
     private func calculateUnSmoothedGlucoseDots(fullSize: CGSize) {
         calculationQueue.async {
         calculationQueue.async {
             let dots = glucose.concurrentMap { value -> CGRect in
             let dots = glucose.concurrentMap { value -> CGRect in
@@ -579,7 +653,7 @@ extension MainChartView {
             let fpus = carbs.filter { $0.isFPU ?? false }
             let fpus = carbs.filter { $0.isFPU ?? false }
             let dots = fpus.map { value -> DotInfo in
             let dots = fpus.map { value -> DotInfo in
                 let center = timeToInterpolatedPoint(value.createdAt.timeIntervalSince1970, fullSize: fullSize)
                 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)
                 let rect = CGRect(x: center.x - size / 2, y: center.y - size / 2, width: size, height: size)
                 return DotInfo(rect: rect, value: value.carbs)
                 return DotInfo(rect: rect, value: value.carbs)
             }
             }

+ 1 - 1
FreeAPS/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift

@@ -48,7 +48,7 @@ struct CurrentGlucoseView: View {
         VStack(alignment: .center) {
         VStack(alignment: .center) {
             HStack {
             HStack {
                 Text(
                 Text(
-                    recentGlucose?.glucose
+                    (recentGlucose?.glucose ?? 100) == 400 ? "HIGH" : recentGlucose?.glucose
                         .map {
                         .map {
                             glucoseFormatter
                             glucoseFormatter
                                 .string(from: Double(units == .mmolL ? $0.asMmolL : Decimal($0)) as NSNumber)! }
                                 .string(from: Double(units == .mmolL ? $0.asMmolL : Decimal($0)) as NSNumber)! }

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

@@ -384,6 +384,7 @@ extension Home {
 
 
                 MainChartView(
                 MainChartView(
                     glucose: $state.glucose,
                     glucose: $state.glucose,
+                    isManual: $state.isManual,
                     suggestion: $state.suggestion,
                     suggestion: $state.suggestion,
                     tempBasals: $state.tempBasals,
                     tempBasals: $state.tempBasals,
                     boluses: $state.boluses,
                     boluses: $state.boluses,
@@ -618,6 +619,9 @@ extension Home {
                         .padding(.bottom, 4)
                         .padding(.bottom, 4)
                         .padding(.top, 8)
                         .padding(.top, 8)
                     Text(errorMessage).font(.caption).foregroundColor(.loopRed)
                     Text(errorMessage).font(.caption).foregroundColor(.loopRed)
+                } else if let suggestion = state.suggestion, (suggestion.bg ?? 100) == 400 {
+                    Text("Invalid CGM reading (HIGH).").font(.callout).bold().foregroundColor(.loopRed).padding(.top, 8)
+                    Text("SMBs and High Temps Disabled.").font(.caption).foregroundColor(.white).padding(.bottom, 4)
                 }
                 }
             }
             }
         }
         }

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

@@ -170,6 +170,35 @@ extension NightscoutAPI {
             .eraseToAnyPublisher()
             .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> {
     func deleteInsulin(at date: Date) -> AnyPublisher<Void, Swift.Error> {
         var components = URLComponents()
         var components = URLComponents()
         components.scheme = url.scheme
         components.scheme = url.scheme
@@ -445,6 +474,30 @@ extension NightscoutAPI {
             .map { _ in () }
             .map { _ in () }
             .eraseToAnyPublisher()
             .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 {
 private extension String {

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

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

+ 1 - 1
FreeAPS/Sources/Services/UserNotifiactions/UserNotificationsManager.swift

@@ -127,7 +127,7 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
     private func scheduleMissingLoopNotifiactions(date _: Date) {
     private func scheduleMissingLoopNotifiactions(date _: Date) {
         ensureCanSendNotification {
         ensureCanSendNotification {
             let title = NSLocalizedString("iAPS not active", comment: "iAPS not active")
             let title = NSLocalizedString("iAPS not active", comment: "iAPS not active")
-            let body = NSLocalizedString("Last loop was more then %d min ago", comment: "Last loop was more then %d min ago")
+            let body = NSLocalizedString("Last loop was more than %d min ago", comment: "Last loop was more than %d min ago")
 
 
             let firstInterval = 20 // min
             let firstInterval = 20 // min
             let secondInterval = 40 // min
             let secondInterval = 40 // min