فهرست منبع

Merge pull request #121 from nightscout/dev

Dev
bjornoleh 2 سال پیش
والد
کامیت
81d6b01b1c

+ 4 - 4
FreeAPS/Sources/APS/Storage/CarbsStorage.swift

@@ -11,7 +11,7 @@ protocol CarbsStorage {
     func storeCarbs(_ carbs: [CarbsEntry])
     func storeCarbs(_ carbs: [CarbsEntry])
     func syncDate() -> Date
     func syncDate() -> Date
     func recent() -> [CarbsEntry]
     func recent() -> [CarbsEntry]
-    func nightscoutTretmentsNotUploaded() -> [NigtscoutTreatment]
+    func nightscoutTretmentsNotUploaded() -> [NightscoutTreatment]
     func deleteCarbs(at date: Date)
     func deleteCarbs(at date: Date)
 }
 }
 
 
@@ -170,12 +170,12 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         }
         }
     }
     }
 
 
-    func nightscoutTretmentsNotUploaded() -> [NigtscoutTreatment] {
-        let uploaded = storage.retrieve(OpenAPS.Nightscout.uploadedPumphistory, as: [NigtscoutTreatment].self) ?? []
+    func nightscoutTretmentsNotUploaded() -> [NightscoutTreatment] {
+        let uploaded = storage.retrieve(OpenAPS.Nightscout.uploadedPumphistory, as: [NightscoutTreatment].self) ?? []
 
 
         let eventsManual = recent().filter { $0.enteredBy == CarbsEntry.manual }
         let eventsManual = recent().filter { $0.enteredBy == CarbsEntry.manual }
         let treatments = eventsManual.map {
         let treatments = eventsManual.map {
-            NigtscoutTreatment(
+            NightscoutTreatment(
                 duration: nil,
                 duration: nil,
                 rawDuration: nil,
                 rawDuration: nil,
                 rawRate: nil,
                 rawRate: nil,

+ 7 - 7
FreeAPS/Sources/APS/Storage/GlucoseStorage.swift

@@ -15,7 +15,7 @@ protocol GlucoseStorage {
     func isGlucoseFresh() -> Bool
     func isGlucoseFresh() -> Bool
     func isGlucoseNotFlat() -> Bool
     func isGlucoseNotFlat() -> Bool
     func nightscoutGlucoseNotUploaded() -> [BloodGlucose]
     func nightscoutGlucoseNotUploaded() -> [BloodGlucose]
-    func nightscoutCGMStateNotUploaded() -> [NigtscoutTreatment]
+    func nightscoutCGMStateNotUploaded() -> [NightscoutTreatment]
     var alarm: GlucoseAlarm? { get }
     var alarm: GlucoseAlarm? { get }
 }
 }
 
 
@@ -85,7 +85,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             debug(.deviceManager, "start storage cgmState")
             debug(.deviceManager, "start storage cgmState")
             self.storage.transaction { storage in
             self.storage.transaction { storage in
                 let file = OpenAPS.Monitor.cgmState
                 let file = OpenAPS.Monitor.cgmState
-                var treatments = storage.retrieve(file, as: [NigtscoutTreatment].self) ?? []
+                var treatments = storage.retrieve(file, as: [NightscoutTreatment].self) ?? []
                 var updated = false
                 var updated = false
                 for x in glucose {
                 for x in glucose {
                     debug(.deviceManager, "storeGlucose \(x)")
                     debug(.deviceManager, "storeGlucose \(x)")
@@ -107,7 +107,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                     if let a = x.activationDate {
                     if let a = x.activationDate {
                         notes = "\(notes) activated on \(a)"
                         notes = "\(notes) activated on \(a)"
                     }
                     }
-                    let treatment = NigtscoutTreatment(
+                    let treatment = NightscoutTreatment(
                         duration: nil,
                         duration: nil,
                         rawDuration: nil,
                         rawDuration: nil,
                         rawRate: nil,
                         rawRate: nil,
@@ -115,7 +115,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                         rate: nil,
                         rate: nil,
                         eventType: .nsSensorChange,
                         eventType: .nsSensorChange,
                         createdAt: sessionStartDate,
                         createdAt: sessionStartDate,
-                        enteredBy: NigtscoutTreatment.local,
+                        enteredBy: NightscoutTreatment.local,
                         bolus: nil,
                         bolus: nil,
                         insulin: nil,
                         insulin: nil,
                         notes: notes,
                         notes: notes,
@@ -212,9 +212,9 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
         return Array(Set(recentGlucose).subtracting(Set(uploaded)))
         return Array(Set(recentGlucose).subtracting(Set(uploaded)))
     }
     }
 
 
-    func nightscoutCGMStateNotUploaded() -> [NigtscoutTreatment] {
-        let uploaded = storage.retrieve(OpenAPS.Nightscout.uploadedCGMState, as: [NigtscoutTreatment].self) ?? []
-        let recent = storage.retrieve(OpenAPS.Monitor.cgmState, as: [NigtscoutTreatment].self) ?? []
+    func nightscoutCGMStateNotUploaded() -> [NightscoutTreatment] {
+        let uploaded = storage.retrieve(OpenAPS.Nightscout.uploadedCGMState, as: [NightscoutTreatment].self) ?? []
+        let recent = storage.retrieve(OpenAPS.Monitor.cgmState, as: [NightscoutTreatment].self) ?? []
 
 
         return Array(Set(recent).subtracting(Set(uploaded)))
         return Array(Set(recent).subtracting(Set(uploaded)))
     }
     }

+ 148 - 141
FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift

@@ -12,7 +12,7 @@ protocol PumpHistoryStorage {
     func storeEvents(_ events: [PumpHistoryEvent])
     func storeEvents(_ events: [PumpHistoryEvent])
     func storeJournalCarbs(_ carbs: Int)
     func storeJournalCarbs(_ carbs: Int)
     func recent() -> [PumpHistoryEvent]
     func recent() -> [PumpHistoryEvent]
-    func nightscoutTretmentsNotUploaded() -> [NigtscoutTreatment]
+    func nightscoutTreatmentsNotUploaded() -> [NightscoutTreatment]
     func saveCancelTempEvents()
     func saveCancelTempEvents()
     func deleteInsulin(at date: Date)
     func deleteInsulin(at date: Date)
 }
 }
@@ -44,7 +44,9 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                         durationMin: nil,
                         durationMin: nil,
                         rate: nil,
                         rate: nil,
                         temp: nil,
                         temp: nil,
-                        carbInput: nil
+                        carbInput: nil,
+                        isSMB: dose.automatic,
+                        isExternalInsulin: dose.manuallyEntered
                     )]
                     )]
                 case .tempBasal:
                 case .tempBasal:
                     guard let dose = event.dose else { return [] }
                     guard let dose = event.dose else { return [] }
@@ -209,155 +211,30 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
         }
         }
     }
     }
 
 
-    func nightscoutTretmentsNotUploaded() -> [NigtscoutTreatment] {
+    func nightscoutTreatmentsNotUploaded() -> [NightscoutTreatment] {
         let events = recent()
         let events = recent()
         guard !events.isEmpty else { return [] }
         guard !events.isEmpty else { return [] }
 
 
-        let temps: [NigtscoutTreatment] = events.reduce([]) { result, event in
-            var result = result
-            switch event.type {
-            case .tempBasal:
-                result.append(NigtscoutTreatment(
-                    duration: nil,
-                    rawDuration: nil,
-                    rawRate: event,
-                    absolute: event.rate,
-                    rate: event.rate,
-                    eventType: .nsTempBasal,
-                    createdAt: event.timestamp,
-                    enteredBy: NigtscoutTreatment.local,
-                    bolus: nil,
-                    insulin: nil,
-                    notes: nil,
-                    carbs: nil,
-                    fat: nil,
-                    protein: nil,
-                    targetTop: nil,
-                    targetBottom: nil
-                ))
-            case .tempBasalDuration:
-                if var last = result.popLast(), last.eventType == .nsTempBasal, last.createdAt == event.timestamp {
-                    last.duration = event.durationMin
-                    last.rawDuration = event
-                    result.append(last)
-                }
-            default: break
-            }
-            return result
-        }
+        var treatments: [NightscoutTreatment?] = []
 
 
-        let bolusesAndCarbs = events.compactMap { event -> NigtscoutTreatment? in
-            switch event.type {
-            case .bolus:
-                return NigtscoutTreatment(
-                    duration: event.duration,
-                    rawDuration: nil,
-                    rawRate: nil,
-                    absolute: nil,
-                    rate: nil,
-                    eventType: .bolus,
-                    createdAt: event.timestamp,
-                    enteredBy: NigtscoutTreatment.local,
-                    bolus: event,
-                    insulin: event.amount,
-                    notes: nil,
-                    carbs: nil,
-                    fat: nil,
-                    protein: nil,
-                    targetTop: nil,
-                    targetBottom: nil
-                )
-            case .journalCarbs:
-                return NigtscoutTreatment(
-                    duration: nil,
-                    rawDuration: nil,
-                    rawRate: nil,
-                    absolute: nil,
-                    rate: nil,
-                    eventType: .nsCarbCorrection,
-                    createdAt: event.timestamp,
-                    enteredBy: NigtscoutTreatment.local,
-                    bolus: nil,
-                    insulin: nil,
-                    notes: nil,
-                    carbs: Decimal(event.carbInput ?? 0),
-                    fat: nil,
-                    protein: nil,
-                    targetTop: nil,
-                    targetBottom: nil
-                )
-            default: return nil
+        for i in 0 ..< events.count {
+            let event = events[i]
+            var nextEvent: PumpHistoryEvent?
+            if i + 1 < events.count {
+                nextEvent = events[i + 1]
             }
             }
-        }
-
-        let misc = events.compactMap { event -> NigtscoutTreatment? in
-            switch event.type {
-            case .prime:
-                return NigtscoutTreatment(
-                    duration: event.duration,
-                    rawDuration: nil,
-                    rawRate: nil,
-                    absolute: nil,
-                    rate: nil,
-                    eventType: .nsSiteChange,
-                    createdAt: event.timestamp,
-                    enteredBy: NigtscoutTreatment.local,
-                    bolus: event,
-                    insulin: nil,
-                    notes: nil,
-                    carbs: nil,
-                    fat: nil,
-                    protein: nil,
-                    targetTop: nil,
-                    targetBottom: nil
-                )
-            case .rewind:
-                return NigtscoutTreatment(
-                    duration: nil,
-                    rawDuration: nil,
-                    rawRate: nil,
-                    absolute: nil,
-                    rate: nil,
-                    eventType: .nsInsulinChange,
-                    createdAt: event.timestamp,
-                    enteredBy: NigtscoutTreatment.local,
-                    bolus: nil,
-                    insulin: nil,
-                    notes: nil,
-                    carbs: nil,
-                    fat: nil,
-                    protein: nil,
-                    targetTop: nil,
-                    targetBottom: nil
-                )
-            case .pumpAlarm:
-                return NigtscoutTreatment(
-                    duration: 30, // minutes
-                    rawDuration: nil,
-                    rawRate: nil,
-                    absolute: nil,
-                    rate: nil,
-                    eventType: .nsAnnouncement,
-                    createdAt: event.timestamp,
-                    enteredBy: NigtscoutTreatment.local,
-                    bolus: nil,
-                    insulin: nil,
-                    notes: "Alarm \(String(describing: event.note)) \(event.type)",
-                    carbs: nil,
-                    fat: nil,
-                    protein: nil,
-                    targetTop: nil,
-                    targetBottom: nil
-                )
-            default: return nil
+            if event.type == .tempBasal, nextEvent?.type == .tempBasalDuration {
+                treatments.append(NightscoutTreatment(event: event, tempBasalDuration: nextEvent))
+            } else {
+                treatments.append(NightscoutTreatment(event: event))
             }
             }
         }
         }
 
 
-        let uploaded = storage.retrieve(OpenAPS.Nightscout.uploadedPumphistory, as: [NigtscoutTreatment].self) ?? []
+        let uploaded = storage.retrieve(OpenAPS.Nightscout.uploadedPumphistory, as: [NightscoutTreatment].self) ?? []
 
 
-        let treatments = Array(Set([bolusesAndCarbs, temps, misc].flatMap { $0 }).subtracting(Set(uploaded)))
+        let treatmentsToUpload = Set(treatments.compactMap { $0 }).subtracting(Set(uploaded))
 
 
-        return treatments.sorted { $0.createdAt! > $1.createdAt! }
+        return treatmentsToUpload.sorted { $0.createdAt! > $1.createdAt! }
     }
     }
 
 
     func saveCancelTempEvents() {
     func saveCancelTempEvents() {
@@ -392,3 +269,133 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
         storeEvents(events)
         storeEvents(events)
     }
     }
 }
 }
+
+extension NightscoutTreatment {
+    init?(event: PumpHistoryEvent, tempBasalDuration: PumpHistoryEvent? = nil) {
+        var basalDurationEvent: PumpHistoryEvent?
+        if tempBasalDuration != nil, tempBasalDuration?.timestamp == event.timestamp, event.type == .tempBasal,
+           tempBasalDuration?.type == .tempBasalDuration
+        {
+            basalDurationEvent = tempBasalDuration
+        }
+        switch event.type {
+        case .tempBasal:
+            self.init(
+                duration: basalDurationEvent?.durationMin,
+                rawDuration: basalDurationEvent,
+                rawRate: event,
+                absolute: event.rate,
+                rate: event.rate,
+                eventType: .nsTempBasal,
+                createdAt: event.timestamp,
+                enteredBy: NightscoutTreatment.local,
+                bolus: nil,
+                insulin: nil,
+                notes: nil,
+                carbs: nil,
+                fat: nil,
+                protein: nil,
+                targetTop: nil,
+                targetBottom: nil
+            )
+        case .bolus:
+            let eventType = determineBolusEventType(for: event)
+            self.init(
+                duration: event.duration,
+                rawDuration: nil,
+                rawRate: nil,
+                absolute: nil,
+                rate: nil,
+                eventType: eventType,
+                createdAt: event.timestamp,
+                enteredBy: NightscoutTreatment.local,
+                bolus: event,
+                insulin: event.amount,
+                notes: nil,
+                carbs: nil,
+                fat: nil,
+                protein: nil,
+                targetTop: nil,
+                targetBottom: nil
+            )
+        case .journalCarbs:
+            self.init(
+                duration: nil,
+                rawDuration: nil,
+                rawRate: nil,
+                absolute: nil,
+                rate: nil,
+                eventType: .nsCarbCorrection,
+                createdAt: event.timestamp,
+                enteredBy: NightscoutTreatment.local,
+                bolus: nil,
+                insulin: nil,
+                notes: nil,
+                carbs: Decimal(event.carbInput ?? 0),
+                fat: nil,
+                protein: nil,
+                targetTop: nil,
+                targetBottom: nil
+            )
+        case .prime:
+            self.init(
+                duration: event.duration,
+                rawDuration: nil,
+                rawRate: nil,
+                absolute: nil,
+                rate: nil,
+                eventType: .nsSiteChange,
+                createdAt: event.timestamp,
+                enteredBy: NightscoutTreatment.local,
+                bolus: event,
+                insulin: nil,
+                notes: nil,
+                carbs: nil,
+                fat: nil,
+                protein: nil,
+                targetTop: nil,
+                targetBottom: nil
+            )
+        case .rewind:
+            self.init(
+                duration: nil,
+                rawDuration: nil,
+                rawRate: nil,
+                absolute: nil,
+                rate: nil,
+                eventType: .nsInsulinChange,
+                createdAt: event.timestamp,
+                enteredBy: NightscoutTreatment.local,
+                bolus: nil,
+                insulin: nil,
+                notes: nil,
+                carbs: nil,
+                fat: nil,
+                protein: nil,
+                targetTop: nil,
+                targetBottom: nil
+            )
+        case .pumpAlarm:
+            self.init(
+                duration: 30, // minutes
+                rawDuration: nil,
+                rawRate: nil,
+                absolute: nil,
+                rate: nil,
+                eventType: .nsAnnouncement,
+                createdAt: event.timestamp,
+                enteredBy: NightscoutTreatment.local,
+                bolus: nil,
+                insulin: nil,
+                notes: "Alarm \(String(describing: event.note)) \(event.type)",
+                carbs: nil,
+                fat: nil,
+                protein: nil,
+                targetTop: nil,
+                targetBottom: nil
+            )
+        default:
+            return nil
+        }
+    }
+}

+ 4 - 4
FreeAPS/Sources/APS/Storage/TempTargetsStorage.swift

@@ -10,7 +10,7 @@ protocol TempTargetsStorage {
     func storeTempTargets(_ targets: [TempTarget])
     func storeTempTargets(_ targets: [TempTarget])
     func syncDate() -> Date
     func syncDate() -> Date
     func recent() -> [TempTarget]
     func recent() -> [TempTarget]
-    func nightscoutTretmentsNotUploaded() -> [NigtscoutTreatment]
+    func nightscoutTretmentsNotUploaded() -> [NightscoutTreatment]
     func storePresets(_ targets: [TempTarget])
     func storePresets(_ targets: [TempTarget])
     func presets() -> [TempTarget]
     func presets() -> [TempTarget]
     func current() -> TempTarget?
     func current() -> TempTarget?
@@ -82,12 +82,12 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
         return last
         return last
     }
     }
 
 
-    func nightscoutTretmentsNotUploaded() -> [NigtscoutTreatment] {
-        let uploaded = storage.retrieve(OpenAPS.Nightscout.uploadedTempTargets, as: [NigtscoutTreatment].self) ?? []
+    func nightscoutTretmentsNotUploaded() -> [NightscoutTreatment] {
+        let uploaded = storage.retrieve(OpenAPS.Nightscout.uploadedTempTargets, as: [NightscoutTreatment].self) ?? []
 
 
         let eventsManual = recent().filter { $0.enteredBy == TempTarget.manual }
         let eventsManual = recent().filter { $0.enteredBy == TempTarget.manual }
         let treatments = eventsManual.map {
         let treatments = eventsManual.map {
-            NigtscoutTreatment(
+            NightscoutTreatment(
                 duration: Int($0.duration),
                 duration: Int($0.duration),
                 rawDuration: nil,
                 rawDuration: nil,
                 rawRate: nil,
                 rawRate: nil,

+ 11 - 4
FreeAPS/Sources/Models/NightscoutTreatment.swift

@@ -1,6 +1,13 @@
 import Foundation
 import Foundation
 
 
-struct NigtscoutTreatment: JSON, Hashable, Equatable {
+func determineBolusEventType(for event: PumpHistoryEvent) -> EventType {
+    if event.isExternalInsulin ?? false {
+        return .nsExternalInsulin
+    }
+    return event.type
+}
+
+struct NightscoutTreatment: JSON, Hashable, Equatable {
     var duration: Int?
     var duration: Int?
     var rawDuration: PumpHistoryEvent?
     var rawDuration: PumpHistoryEvent?
     var rawRate: PumpHistoryEvent?
     var rawRate: PumpHistoryEvent?
@@ -21,9 +28,9 @@ struct NigtscoutTreatment: JSON, Hashable, Equatable {
 
 
     static let local = "Open-iAPS"
     static let local = "Open-iAPS"
 
 
-    static let empty = NigtscoutTreatment(from: "{}")!
+    static let empty = NightscoutTreatment(from: "{}")!
 
 
-    static func == (lhs: NigtscoutTreatment, rhs: NigtscoutTreatment) -> Bool {
+    static func == (lhs: NightscoutTreatment, rhs: NightscoutTreatment) -> Bool {
         (lhs.createdAt ?? Date()) == (rhs.createdAt ?? Date())
         (lhs.createdAt ?? Date()) == (rhs.createdAt ?? Date())
     }
     }
 
 
@@ -32,7 +39,7 @@ struct NigtscoutTreatment: JSON, Hashable, Equatable {
     }
     }
 }
 }
 
 
-extension NigtscoutTreatment {
+extension NightscoutTreatment {
     private enum CodingKeys: String, CodingKey {
     private enum CodingKeys: String, CodingKey {
         case duration
         case duration
         case rawDuration = "raw_duration"
         case rawDuration = "raw_duration"

+ 11 - 2
FreeAPS/Sources/Models/PumpHistoryEvent.swift

@@ -12,6 +12,8 @@ struct PumpHistoryEvent: JSON, Equatable {
     let temp: TempType?
     let temp: TempType?
     let carbInput: Int?
     let carbInput: Int?
     let note: String?
     let note: String?
+    let isSMB: Bool?
+    let isExternalInsulin: Bool?
 
 
     init(
     init(
         id: String,
         id: String,
@@ -23,7 +25,9 @@ struct PumpHistoryEvent: JSON, Equatable {
         rate: Decimal? = nil,
         rate: Decimal? = nil,
         temp: TempType? = nil,
         temp: TempType? = nil,
         carbInput: Int? = nil,
         carbInput: Int? = nil,
-        note: String? = nil
+        note: String? = nil,
+        isSMB: Bool? = nil,
+        isExternalInsulin: Bool? = nil
     ) {
     ) {
         self.id = id
         self.id = id
         self.type = type
         self.type = type
@@ -35,12 +39,14 @@ struct PumpHistoryEvent: JSON, Equatable {
         self.temp = temp
         self.temp = temp
         self.carbInput = carbInput
         self.carbInput = carbInput
         self.note = note
         self.note = note
+        self.isSMB = isSMB
+        self.isExternalInsulin = isExternalInsulin
     }
     }
 }
 }
 
 
 enum EventType: String, JSON {
 enum EventType: String, JSON {
     case bolus = "Bolus"
     case bolus = "Bolus"
-    case mealBulus = "Meal Bolus"
+    case mealBolus = "Meal Bolus"
     case correctionBolus = "Correction Bolus"
     case correctionBolus = "Correction Bolus"
     case snackBolus = "Snack Bolus"
     case snackBolus = "Snack Bolus"
     case bolusWizard = "BolusWizard"
     case bolusWizard = "BolusWizard"
@@ -62,6 +68,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 nsExternalInsulin = "External Insulin"
 }
 }
 
 
 enum TempType: String, JSON {
 enum TempType: String, JSON {
@@ -81,6 +88,8 @@ extension PumpHistoryEvent {
         case temp
         case temp
         case carbInput = "carb_input"
         case carbInput = "carb_input"
         case note
         case note
+        case isSMB
+        case isExternalInsulin
     }
     }
 }
 }
 
 

+ 2 - 1
FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift

@@ -83,7 +83,8 @@ extension Bolus {
                         durationMin: nil,
                         durationMin: nil,
                         rate: nil,
                         rate: nil,
                         temp: nil,
                         temp: nil,
-                        carbInput: nil
+                        carbInput: nil,
+                        isExternalInsulin: true
                     )
                     )
                 ]
                 ]
             )
             )

+ 3 - 25
FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift

@@ -81,33 +81,11 @@ extension Bolus {
                         label: { Text("Enact bolus") }
                         label: { Text("Enact bolus") }
                             .disabled(state.amount <= 0)
                             .disabled(state.amount <= 0)
                     }
                     }
-                    Section {
-                        if waitForSuggestion {
+                    if waitForSuggestion {
+                        Section {
                             Button { state.showModal(for: nil) }
                             Button { state.showModal(for: nil) }
                             label: { Text("Continue without bolus") }
                             label: { Text("Continue without bolus") }
-                        } else {
-                            Button { isAddInsulinAlertPresented = true }
-                            label: { Text("Add insulin without actually bolusing") }
-                                .disabled(state.amount <= 0)
-                        }
-                    }
-                    .alert(isPresented: $isAddInsulinAlertPresented) {
-                        Alert(
-                            title: Text("Are you sure?"),
-                            message: Text(
-                                NSLocalizedString("Add", comment: "Add insulin without bolusing alert") + " " + formatter
-                                    .string(from: state.amount as NSNumber)! + NSLocalizedString(" U", comment: "Insulin unit") +
-                                    NSLocalizedString(" without bolusing", comment: "Add insulin without bolusing alert")
-                            ),
-                            primaryButton: .destructive(
-                                Text("Add"),
-                                action: {
-                                    state.addWithoutBolus()
-                                    isAddInsulinAlertPresented = false
-                                }
-                            ),
-                            secondaryButton: .cancel()
-                        )
+                        }.frame(maxWidth: .infinity, alignment: .center)
                     }
                     }
                 }
                 }
             }
             }

+ 23 - 8
FreeAPS/Sources/Modules/DataTable/DataTableDataFlow.swift

@@ -66,8 +66,10 @@ enum DataTable {
         let isFPU: Bool?
         let isFPU: Bool?
         let fpuID: String?
         let fpuID: String?
         let note: String?
         let note: String?
+        let isSMB: Bool?
+        let isExternal: Bool?
 
 
-        private var numberFormater: NumberFormatter {
+        private var numberFormatter: NumberFormatter {
             let formatter = NumberFormatter()
             let formatter = NumberFormatter()
             formatter.numberStyle = .decimal
             formatter.numberStyle = .decimal
             formatter.maximumFractionDigits = 2
             formatter.maximumFractionDigits = 2
@@ -92,7 +94,9 @@ enum DataTable {
             idPumpEvent: String? = nil,
             idPumpEvent: String? = nil,
             isFPU: Bool? = false,
             isFPU: Bool? = false,
             fpuID: String? = nil,
             fpuID: String? = nil,
-            note: String? = nil
+            note: String? = nil,
+            isSMB: Bool? = nil,
+            isExternal: Bool? = nil
         ) {
         ) {
             self.units = units
             self.units = units
             self.type = type
             self.type = type
@@ -105,6 +109,8 @@ enum DataTable {
             self.isFPU = isFPU
             self.isFPU = isFPU
             self.fpuID = fpuID
             self.fpuID = fpuID
             self.note = note
             self.note = note
+            self.isSMB = isSMB
+            self.isExternal = isExternal
         }
         }
 
 
         static func == (lhs: Treatment, rhs: Treatment) -> Bool {
         static func == (lhs: Treatment, rhs: Treatment) -> Bool {
@@ -126,14 +132,23 @@ enum DataTable {
 
 
             switch type {
             switch type {
             case .carbs:
             case .carbs:
-                return numberFormater.string(from: amount as NSNumber)! + NSLocalizedString(" g", comment: "gram of carbs")
+                return numberFormatter.string(from: amount as NSNumber)! + NSLocalizedString(" g", comment: "gram of carbs")
             case .fpus:
             case .fpus:
-                return numberFormater
+                return numberFormatter
                     .string(from: amount as NSNumber)! + NSLocalizedString(" g", comment: "gram of carb equilvalents")
                     .string(from: amount as NSNumber)! + NSLocalizedString(" g", comment: "gram of carb equilvalents")
             case .bolus:
             case .bolus:
-                return numberFormater.string(from: amount as NSNumber)! + NSLocalizedString(" U", comment: "Insulin unit")
+                var bolusText = ""
+
+                if isExternal ?? false {
+                    bolusText += " " + NSLocalizedString("External", comment: "External Insulin")
+                } else if isSMB ?? false {
+                    bolusText += " " + NSLocalizedString("SMB", comment: "SMB")
+                }
+
+                return numberFormatter
+                    .string(from: amount as NSNumber)! + NSLocalizedString(" U", comment: "Insulin unit") + bolusText
             case .tempBasal:
             case .tempBasal:
-                return numberFormater
+                return numberFormatter
                     .string(from: amount as NSNumber)! + NSLocalizedString(" U/hr", comment: "Unit insulin per hour")
                     .string(from: amount as NSNumber)! + NSLocalizedString(" U/hr", comment: "Unit insulin per hour")
             case .tempTarget:
             case .tempTarget:
                 var converted = amount
                 var converted = amount
@@ -142,7 +157,7 @@ enum DataTable {
                 }
                 }
 
 
                 guard var secondAmount = secondAmount else {
                 guard var secondAmount = secondAmount else {
-                    return numberFormater.string(from: converted as NSNumber)! + " \(units.rawValue)"
+                    return numberFormatter.string(from: converted as NSNumber)! + " \(units.rawValue)"
                 }
                 }
                 if units == .mmolL {
                 if units == .mmolL {
                     secondAmount = secondAmount.asMmolL
                     secondAmount = secondAmount.asMmolL
@@ -177,7 +192,7 @@ enum DataTable {
             guard let duration = duration, duration > 0 else {
             guard let duration = duration, duration > 0 else {
                 return nil
                 return nil
             }
             }
-            return numberFormater.string(from: duration as NSNumber)! + " min"
+            return numberFormatter.string(from: duration as NSNumber)! + " min"
         }
         }
     }
     }
 
 

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

@@ -14,6 +14,12 @@ extension DataTable {
             pumpHistoryStorage.recent()
             pumpHistoryStorage.recent()
         }
         }
 
 
+        func pumpSettings() -> PumpSettings {
+            storage.retrieve(OpenAPS.Settings.settings, as: PumpSettings.self)
+                ?? PumpSettings(from: OpenAPS.defaults(for: OpenAPS.Settings.settings))
+                ?? PumpSettings(insulinActionCurve: 6, maxBolus: 10, maxBasal: 2)
+        }
+
         func tempTargets() -> [TempTarget] {
         func tempTargets() -> [TempTarget] {
             tempTargetsStorage.recent()
             tempTargetsStorage.recent()
         }
         }

+ 48 - 1
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 pumpHistoryStorage: PumpHistoryStorage!
 
 
         let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
         let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
 
 
@@ -13,11 +14,15 @@ extension DataTable {
         @Published var treatments: [Treatment] = []
         @Published var treatments: [Treatment] = []
         @Published var glucose: [Glucose] = []
         @Published var glucose: [Glucose] = []
         @Published var manualGlcuose: Decimal = 0
         @Published var manualGlcuose: Decimal = 0
+        @Published var maxBolus: Decimal = 0
+        @Published var externalInsulinAmount: Decimal = 0
+        @Published var externalInsulinDate = Date()
 
 
         var units: GlucoseUnits = .mmolL
         var units: GlucoseUnits = .mmolL
 
 
         override func subscribe() {
         override func subscribe() {
             units = settingsManager.settings.units
             units = settingsManager.settings.units
+            maxBolus = provider.pumpSettings().maxBolus
             setupTreatments()
             setupTreatments()
             setupGlucose()
             setupGlucose()
             broadcaster.register(SettingsObserver.self, observer: self)
             broadcaster.register(SettingsObserver.self, observer: self)
@@ -72,7 +77,15 @@ extension DataTable {
                 let boluses = self.provider.pumpHistory()
                 let boluses = self.provider.pumpHistory()
                     .filter { $0.type == .bolus }
                     .filter { $0.type == .bolus }
                     .map {
                     .map {
-                        Treatment(units: units, type: .bolus, date: $0.timestamp, amount: $0.amount, idPumpEvent: $0.id)
+                        Treatment(
+                            units: units,
+                            type: .bolus,
+                            date: $0.timestamp,
+                            amount: $0.amount,
+                            idPumpEvent: $0.id,
+                            isSMB: $0.isSMB,
+                            isExternal: $0.isExternalInsulin
+                        )
                     }
                     }
 
 
                 let tempBasals = self.provider.pumpHistory()
                 let tempBasals = self.provider.pumpHistory()
@@ -187,6 +200,40 @@ extension DataTable {
             provider.glucoseStorage.storeGlucose([saveToJSON])
             provider.glucoseStorage.storeGlucose([saveToJSON])
             debug(.default, "Manual Glucose saved to glucose.json")
             debug(.default, "Manual Glucose saved to glucose.json")
         }
         }
+
+        func addExternalInsulin() {
+            guard externalInsulinAmount > 0 else {
+                showModal(for: nil)
+                return
+            }
+
+            externalInsulinAmount = min(externalInsulinAmount, maxBolus * 3) // Allow for 3 * Max Bolus for external insulin
+            unlockmanager.unlock()
+                .sink { _ in } receiveValue: { [weak self] _ in
+                    guard let self = self else { return }
+                    pumpHistoryStorage.storeEvents(
+                        [
+                            PumpHistoryEvent(
+                                id: UUID().uuidString,
+                                type: .bolus,
+                                timestamp: externalInsulinDate,
+                                amount: externalInsulinAmount,
+                                duration: nil,
+                                durationMin: nil,
+                                rate: nil,
+                                temp: nil,
+                                carbInput: nil,
+                                isExternalInsulin: true
+                            )
+                        ]
+                    )
+                    debug(.default, "External insulin saved to pumphistory.json")
+
+                    // Reset amount to 0 for next entry
+                    externalInsulinAmount = 0
+                }
+                .store(in: &lifetime)
+        }
     }
     }
 }
 }
 
 

+ 105 - 2
FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift

@@ -12,9 +12,18 @@ 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 showExternalInsulin = false
+        @State private var isAmountUnconfirmed = true
 
 
         @Environment(\.colorScheme) var colorScheme
         @Environment(\.colorScheme) var colorScheme
 
 
+        private var insulinFormatter: NumberFormatter {
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            formatter.maximumFractionDigits = 2
+            return formatter
+        }
+
         private var glucoseFormatter: NumberFormatter {
         private var glucoseFormatter: NumberFormatter {
             let formatter = NumberFormatter()
             let formatter = NumberFormatter()
             formatter.numberStyle = .decimal
             formatter.numberStyle = .decimal
@@ -57,6 +66,14 @@ extension DataTable {
                 leading: Button("Close", action: state.hideModal),
                 leading: Button("Close", action: state.hideModal),
                 trailing: state.mode == .glucose ? EditButton().asAny() : EmptyView().asAny()
                 trailing: state.mode == .glucose ? EditButton().asAny() : EmptyView().asAny()
             )
             )
+            .sheet(isPresented: $showExternalInsulin, onDismiss: {
+                if isAmountUnconfirmed {
+                    state.externalInsulinAmount = 0
+                    state.externalInsulinDate = Date()
+                }
+            }) {
+                addExternalInsulinView
+            }
             .popup(isPresented: newGlucose, alignment: .top, direction: .bottom) {
             .popup(isPresented: newGlucose, alignment: .top, direction: .bottom) {
                 VStack(spacing: 20) {
                 VStack(spacing: 20) {
                     HStack {
                     HStack {
@@ -90,8 +107,30 @@ extension DataTable {
 
 
         private var treatmentsList: some View {
         private var treatmentsList: some View {
             List {
             List {
-                ForEach(state.treatments) { item in
-                    treatmentView(item)
+                HStack {
+                    Spacer()
+                    Button(action: { showExternalInsulin = true
+                        state.externalInsulinDate = Date() }, label: {
+                        HStack {
+                            Text("Add")
+                                .foregroundColor(Color.secondary)
+                                .font(.caption)
+
+                            Image(systemName: "syringe")
+                                .foregroundColor(Color.accentColor)
+                        }.frame(maxWidth: .infinity, alignment: .trailing)
+
+                    }).buttonStyle(.borderless)
+                }
+
+                if !state.treatments.isEmpty {
+                    ForEach(state.treatments) { item in
+                        treatmentView(item)
+                    }
+                } else {
+                    HStack {
+                        Text("No data.")
+                    }
                 }
                 }
             }
             }
         }
         }
@@ -191,6 +230,70 @@ extension DataTable {
             }
             }
         }
         }
 
 
+        var addExternalInsulinView: some View {
+            NavigationView {
+                VStack {
+                    Form {
+                        Section {
+                            HStack {
+                                Text("Amount")
+                                Spacer()
+                                DecimalTextField(
+                                    "0",
+                                    value: $state.externalInsulinAmount,
+                                    formatter: insulinFormatter,
+                                    autofocus: true,
+                                    cleanInput: true
+                                )
+                                Text("U").foregroundColor(.secondary)
+                            }
+                        }
+
+                        Section {
+                            DatePicker("Date", selection: $state.externalInsulinDate, in: ...Date())
+                        }
+
+                        let amountWarningCondition = (state.externalInsulinAmount > state.maxBolus) &&
+                            (state.externalInsulinAmount <= state.maxBolus * 3)
+
+                        Section {
+                            HStack {
+                                Button {
+                                    state.addExternalInsulin()
+                                    isAmountUnconfirmed = false
+                                    showExternalInsulin = false
+                                }
+                                label: {
+                                    Text("Log external insulin")
+                                }
+                                .foregroundColor(amountWarningCondition ? Color.white : Color.accentColor)
+                                .frame(maxWidth: .infinity, alignment: .center)
+                                .disabled(
+                                    state.externalInsulinAmount <= 0 || state.externalInsulinAmount > state
+                                        .maxBolus * 3
+                                )
+                            }
+                        }
+                        header: {
+                            if amountWarningCondition
+                            {
+                                Text("⚠️ Warning! The entered insulin amount is greater than your Max Bolus setting!")
+                            }
+                        }
+                        .listRowBackground(
+                            amountWarningCondition ? Color
+                                .red : colorScheme == .dark ? Color(UIColor.secondarySystemBackground) : Color.white
+                        )
+                    }
+                }
+                .onAppear(perform: configureView)
+                .navigationTitle("External Insulin")
+                .navigationBarTitleDisplayMode(.inline)
+                .navigationBarItems(leading: Button("Close", action: { showExternalInsulin = false
+                    state.externalInsulinAmount = 0 }))
+            }
+        }
+
         @ViewBuilder private func glucoseView(_ item: Glucose) -> some View {
         @ViewBuilder private func glucoseView(_ item: Glucose) -> some View {
             VStack(alignment: .leading, spacing: 4) {
             VStack(alignment: .leading, spacing: 4) {
                 HStack {
                 HStack {

+ 3 - 3
FreeAPS/Sources/Services/Network/NightscoutAPI.swift

@@ -112,7 +112,7 @@ extension NightscoutAPI {
             ),
             ),
             URLQueryItem(
             URLQueryItem(
                 name: "find[enteredBy][$ne]",
                 name: "find[enteredBy][$ne]",
-                value: NigtscoutTreatment.local.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
+                value: NightscoutTreatment.local.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
             )
             )
         ]
         ]
         if let date = sinceDate {
         if let date = sinceDate {
@@ -213,7 +213,7 @@ extension NightscoutAPI {
             ),
             ),
             URLQueryItem(
             URLQueryItem(
                 name: "find[enteredBy][$ne]",
                 name: "find[enteredBy][$ne]",
-                value: NigtscoutTreatment.local.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
+                value: NightscoutTreatment.local.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
             ),
             ),
             URLQueryItem(name: "find[duration][$exists]", value: "true")
             URLQueryItem(name: "find[duration][$exists]", value: "true")
         ]
         ]
@@ -278,7 +278,7 @@ extension NightscoutAPI {
             .eraseToAnyPublisher()
             .eraseToAnyPublisher()
     }
     }
 
 
-    func uploadTreatments(_ treatments: [NigtscoutTreatment]) -> AnyPublisher<Void, Swift.Error> {
+    func uploadTreatments(_ treatments: [NightscoutTreatment]) -> AnyPublisher<Void, Swift.Error> {
         var components = URLComponents()
         var components = URLComponents()
         components.scheme = url.scheme
         components.scheme = url.scheme
         components.host = url.host
         components.host = url.host

+ 7 - 7
FreeAPS/Sources/Services/Network/NightscoutManager.swift

@@ -369,7 +369,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         var status: NightscoutStatus
         var status: NightscoutStatus
 
 
         status = NightscoutStatus(
         status = NightscoutStatus(
-            device: NigtscoutTreatment.local,
+            device: NightscoutTreatment.local,
             openaps: openapsStatus,
             openaps: openapsStatus,
             pump: pump,
             pump: pump,
             uploader: uploader
             uploader: uploader
@@ -398,11 +398,11 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     }
     }
 
 
     func uploadPodAge() {
     func uploadPodAge() {
-        let uploadedPodAge = storage.retrieve(OpenAPS.Nightscout.uploadedPodAge, as: [NigtscoutTreatment].self) ?? []
+        let uploadedPodAge = storage.retrieve(OpenAPS.Nightscout.uploadedPodAge, as: [NightscoutTreatment].self) ?? []
         if let podAge = storage.retrieve(OpenAPS.Monitor.podAge, as: Date.self),
         if let podAge = storage.retrieve(OpenAPS.Monitor.podAge, as: Date.self),
            uploadedPodAge.last?.createdAt == nil || podAge != uploadedPodAge.last!.createdAt!
            uploadedPodAge.last?.createdAt == nil || podAge != uploadedPodAge.last!.createdAt!
         {
         {
-            let siteTreatment = NigtscoutTreatment(
+            let siteTreatment = NightscoutTreatment(
                 duration: nil,
                 duration: nil,
                 rawDuration: nil,
                 rawDuration: nil,
                 rawRate: nil,
                 rawRate: nil,
@@ -410,7 +410,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                 rate: nil,
                 rate: nil,
                 eventType: .nsSiteChange,
                 eventType: .nsSiteChange,
                 createdAt: podAge,
                 createdAt: podAge,
-                enteredBy: NigtscoutTreatment.local,
+                enteredBy: NightscoutTreatment.local,
                 bolus: nil,
                 bolus: nil,
                 insulin: nil,
                 insulin: nil,
                 notes: nil,
                 notes: nil,
@@ -528,7 +528,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             startDate: now,
             startDate: now,
             mills: Int(now.timeIntervalSince1970) * 1000,
             mills: Int(now.timeIntervalSince1970) * 1000,
             units: nsUnits,
             units: nsUnits,
-            enteredBy: NigtscoutTreatment.local,
+            enteredBy: NightscoutTreatment.local,
             store: [defaultProfile: ps]
             store: [defaultProfile: ps]
         )
         )
 
 
@@ -578,7 +578,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     }
     }
 
 
     private func uploadPumpHistory() {
     private func uploadPumpHistory() {
-        uploadTreatments(pumpHistoryStorage.nightscoutTretmentsNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedPumphistory)
+        uploadTreatments(pumpHistoryStorage.nightscoutTreatmentsNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedPumphistory)
     }
     }
 
 
     private func uploadCarbs() {
     private func uploadCarbs() {
@@ -622,7 +622,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         }
         }
     }
     }
 
 
-    private func uploadTreatments(_ treatments: [NigtscoutTreatment], fileToSave: String) {
+    private func uploadTreatments(_ treatments: [NightscoutTreatment], fileToSave: String) {
         guard !treatments.isEmpty, let nightscout = nightscoutAPI, isUploadEnabled else {
         guard !treatments.isEmpty, let nightscout = nightscoutAPI, isUploadEnabled else {
             return
             return
         }
         }