diff --git a/FreeAPS/Sources/APS/Storage/CarbsStorage.swift b/FreeAPS/Sources/APS/Storage/CarbsStorage.swift index c8abbdcde..5819a987d 100644 --- a/FreeAPS/Sources/APS/Storage/CarbsStorage.swift +++ b/FreeAPS/Sources/APS/Storage/CarbsStorage.swift @@ -11,7 +11,7 @@ protocol CarbsStorage { func storeCarbs(_ carbs: [CarbsEntry]) func syncDate() -> Date func recent() -> [CarbsEntry] - func nightscoutTretmentsNotUploaded() -> [NigtscoutTreatment] + func nightscoutTretmentsNotUploaded() -> [NightscoutTreatment] 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 treatments = eventsManual.map { - NigtscoutTreatment( + NightscoutTreatment( duration: nil, rawDuration: nil, rawRate: nil, diff --git a/FreeAPS/Sources/APS/Storage/GlucoseStorage.swift b/FreeAPS/Sources/APS/Storage/GlucoseStorage.swift index 35d27dbcd..211057d23 100644 --- a/FreeAPS/Sources/APS/Storage/GlucoseStorage.swift +++ b/FreeAPS/Sources/APS/Storage/GlucoseStorage.swift @@ -15,7 +15,7 @@ protocol GlucoseStorage { func isGlucoseFresh() -> Bool func isGlucoseNotFlat() -> Bool func nightscoutGlucoseNotUploaded() -> [BloodGlucose] - func nightscoutCGMStateNotUploaded() -> [NigtscoutTreatment] + func nightscoutCGMStateNotUploaded() -> [NightscoutTreatment] var alarm: GlucoseAlarm? { get } } @@ -85,7 +85,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable { debug(.deviceManager, "start storage cgmState") self.storage.transaction { storage in let file = OpenAPS.Monitor.cgmState - var treatments = storage.retrieve(file, as: [NigtscoutTreatment].self) ?? [] + var treatments = storage.retrieve(file, as: [NightscoutTreatment].self) ?? [] var updated = false for x in glucose { debug(.deviceManager, "storeGlucose \(x)") @@ -107,7 +107,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable { if let a = x.activationDate { notes = "\(notes) activated on \(a)" } - let treatment = NigtscoutTreatment( + let treatment = NightscoutTreatment( duration: nil, rawDuration: nil, rawRate: nil, @@ -115,7 +115,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable { rate: nil, eventType: .nsSensorChange, createdAt: sessionStartDate, - enteredBy: NigtscoutTreatment.local, + enteredBy: NightscoutTreatment.local, bolus: nil, insulin: nil, notes: notes, @@ -212,9 +212,9 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable { 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))) } diff --git a/FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift b/FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift index 0a2a5ce25..489220de7 100644 --- a/FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift +++ b/FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift @@ -12,7 +12,7 @@ protocol PumpHistoryStorage { func storeEvents(_ events: [PumpHistoryEvent]) func storeJournalCarbs(_ carbs: Int) func recent() -> [PumpHistoryEvent] - func nightscoutTretmentsNotUploaded() -> [NigtscoutTreatment] + func nightscoutTreatmentsNotUploaded() -> [NightscoutTreatment] func saveCancelTempEvents() func deleteInsulin(at date: Date) } @@ -44,7 +44,9 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable { durationMin: nil, rate: nil, temp: nil, - carbInput: nil + carbInput: nil, + isSMB: dose.automatic, + isExternalInsulin: dose.manuallyEntered )] case .tempBasal: 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() 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() { @@ -392,3 +269,133 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable { 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 + } + } +} diff --git a/FreeAPS/Sources/APS/Storage/TempTargetsStorage.swift b/FreeAPS/Sources/APS/Storage/TempTargetsStorage.swift index cea04d080..326abc70e 100644 --- a/FreeAPS/Sources/APS/Storage/TempTargetsStorage.swift +++ b/FreeAPS/Sources/APS/Storage/TempTargetsStorage.swift @@ -10,7 +10,7 @@ protocol TempTargetsStorage { func storeTempTargets(_ targets: [TempTarget]) func syncDate() -> Date func recent() -> [TempTarget] - func nightscoutTretmentsNotUploaded() -> [NigtscoutTreatment] + func nightscoutTretmentsNotUploaded() -> [NightscoutTreatment] func storePresets(_ targets: [TempTarget]) func presets() -> [TempTarget] func current() -> TempTarget? @@ -82,12 +82,12 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable { 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 treatments = eventsManual.map { - NigtscoutTreatment( + NightscoutTreatment( duration: Int($0.duration), rawDuration: nil, rawRate: nil, diff --git a/FreeAPS/Sources/Models/NightscoutTreatment.swift b/FreeAPS/Sources/Models/NightscoutTreatment.swift index d025b9e1e..b30cc20e1 100644 --- a/FreeAPS/Sources/Models/NightscoutTreatment.swift +++ b/FreeAPS/Sources/Models/NightscoutTreatment.swift @@ -1,6 +1,13 @@ 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 rawDuration: PumpHistoryEvent? var rawRate: PumpHistoryEvent? @@ -21,9 +28,9 @@ struct NigtscoutTreatment: JSON, Hashable, Equatable { 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()) } @@ -32,7 +39,7 @@ struct NigtscoutTreatment: JSON, Hashable, Equatable { } } -extension NigtscoutTreatment { +extension NightscoutTreatment { private enum CodingKeys: String, CodingKey { case duration case rawDuration = "raw_duration" diff --git a/FreeAPS/Sources/Models/PumpHistoryEvent.swift b/FreeAPS/Sources/Models/PumpHistoryEvent.swift index fc42597b7..283358998 100644 --- a/FreeAPS/Sources/Models/PumpHistoryEvent.swift +++ b/FreeAPS/Sources/Models/PumpHistoryEvent.swift @@ -12,6 +12,8 @@ struct PumpHistoryEvent: JSON, Equatable { let temp: TempType? let carbInput: Int? let note: String? + let isSMB: Bool? + let isExternalInsulin: Bool? init( id: String, @@ -23,7 +25,9 @@ struct PumpHistoryEvent: JSON, Equatable { rate: Decimal? = nil, temp: TempType? = nil, carbInput: Int? = nil, - note: String? = nil + note: String? = nil, + isSMB: Bool? = nil, + isExternalInsulin: Bool? = nil ) { self.id = id self.type = type @@ -35,12 +39,14 @@ struct PumpHistoryEvent: JSON, Equatable { self.temp = temp self.carbInput = carbInput self.note = note + self.isSMB = isSMB + self.isExternalInsulin = isExternalInsulin } } enum EventType: String, JSON { case bolus = "Bolus" - case mealBulus = "Meal Bolus" + case mealBolus = "Meal Bolus" case correctionBolus = "Correction Bolus" case snackBolus = "Snack Bolus" case bolusWizard = "BolusWizard" @@ -62,6 +68,7 @@ enum EventType: String, JSON { case nsBatteryChange = "Pump Battery Change" case nsAnnouncement = "Announcement" case nsSensorChange = "Sensor Start" + case nsExternalInsulin = "External Insulin" } enum TempType: String, JSON { @@ -81,6 +88,8 @@ extension PumpHistoryEvent { case temp case carbInput = "carb_input" case note + case isSMB + case isExternalInsulin } } diff --git a/FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift b/FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift index c24119b3d..8c4a5a97d 100644 --- a/FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift +++ b/FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift @@ -83,7 +83,8 @@ extension Bolus { durationMin: nil, rate: nil, temp: nil, - carbInput: nil + carbInput: nil, + isExternalInsulin: true ) ] ) diff --git a/FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift b/FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift index 99dff4a33..ca76fa8c7 100644 --- a/FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift +++ b/FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift @@ -81,33 +81,11 @@ extension Bolus { label: { Text("Enact bolus") } .disabled(state.amount <= 0) } - Section { - if waitForSuggestion { + if waitForSuggestion { + Section { Button { state.showModal(for: nil) } 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) } } } diff --git a/FreeAPS/Sources/Modules/DataTable/DataTableDataFlow.swift b/FreeAPS/Sources/Modules/DataTable/DataTableDataFlow.swift index 974ced7e8..4fe22ba99 100644 --- a/FreeAPS/Sources/Modules/DataTable/DataTableDataFlow.swift +++ b/FreeAPS/Sources/Modules/DataTable/DataTableDataFlow.swift @@ -66,8 +66,10 @@ enum DataTable { let isFPU: Bool? let fpuID: String? let note: String? + let isSMB: Bool? + let isExternal: Bool? - private var numberFormater: NumberFormatter { + private var numberFormatter: NumberFormatter { let formatter = NumberFormatter() formatter.numberStyle = .decimal formatter.maximumFractionDigits = 2 @@ -92,7 +94,9 @@ enum DataTable { idPumpEvent: String? = nil, isFPU: Bool? = false, fpuID: String? = nil, - note: String? = nil + note: String? = nil, + isSMB: Bool? = nil, + isExternal: Bool? = nil ) { self.units = units self.type = type @@ -105,6 +109,8 @@ enum DataTable { self.isFPU = isFPU self.fpuID = fpuID self.note = note + self.isSMB = isSMB + self.isExternal = isExternal } static func == (lhs: Treatment, rhs: Treatment) -> Bool { @@ -126,14 +132,23 @@ enum DataTable { switch type { 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: - return numberFormater + return numberFormatter .string(from: amount as NSNumber)! + NSLocalizedString(" g", comment: "gram of carb equilvalents") 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: - return numberFormater + return numberFormatter .string(from: amount as NSNumber)! + NSLocalizedString(" U/hr", comment: "Unit insulin per hour") case .tempTarget: var converted = amount @@ -142,7 +157,7 @@ enum DataTable { } 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 { secondAmount = secondAmount.asMmolL @@ -177,7 +192,7 @@ enum DataTable { guard let duration = duration, duration > 0 else { return nil } - return numberFormater.string(from: duration as NSNumber)! + " min" + return numberFormatter.string(from: duration as NSNumber)! + " min" } } diff --git a/FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift b/FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift index f17b0adf1..35ea2a13a 100644 --- a/FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift +++ b/FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift @@ -14,6 +14,12 @@ extension DataTable { 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] { tempTargetsStorage.recent() } diff --git a/FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift b/FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift index 738245a2a..b828e908c 100644 --- a/FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift +++ b/FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift @@ -6,6 +6,7 @@ extension DataTable { @Injected() var broadcaster: Broadcaster! @Injected() var unlockmanager: UnlockManager! @Injected() private var storage: FileStorage! + @Injected() var pumpHistoryStorage: PumpHistoryStorage! let coredataContext = CoreDataStack.shared.persistentContainer.viewContext @@ -13,11 +14,15 @@ extension DataTable { @Published var treatments: [Treatment] = [] @Published var glucose: [Glucose] = [] @Published var manualGlcuose: Decimal = 0 + @Published var maxBolus: Decimal = 0 + @Published var externalInsulinAmount: Decimal = 0 + @Published var externalInsulinDate = Date() var units: GlucoseUnits = .mmolL override func subscribe() { units = settingsManager.settings.units + maxBolus = provider.pumpSettings().maxBolus setupTreatments() setupGlucose() broadcaster.register(SettingsObserver.self, observer: self) @@ -72,7 +77,15 @@ extension DataTable { let boluses = self.provider.pumpHistory() .filter { $0.type == .bolus } .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() @@ -187,6 +200,40 @@ extension DataTable { provider.glucoseStorage.storeGlucose([saveToJSON]) 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) + } } } diff --git a/FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift b/FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift index 073884ed6..8f7eafc68 100644 --- a/FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift +++ b/FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift @@ -12,9 +12,18 @@ extension DataTable { @State private var isRemoveInsulinAlertPresented = false @State private var removeInsulinAlert: Alert? @State private var newGlucose = false + @State private var showExternalInsulin = false + @State private var isAmountUnconfirmed = true @Environment(\.colorScheme) var colorScheme + private var insulinFormatter: NumberFormatter { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.maximumFractionDigits = 2 + return formatter + } + private var glucoseFormatter: NumberFormatter { let formatter = NumberFormatter() formatter.numberStyle = .decimal @@ -57,6 +66,14 @@ extension DataTable { leading: Button("Close", action: state.hideModal), 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) { VStack(spacing: 20) { HStack { @@ -90,8 +107,30 @@ extension DataTable { private var treatmentsList: some View { 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 { VStack(alignment: .leading, spacing: 4) { HStack { diff --git a/FreeAPS/Sources/Services/Network/NightscoutAPI.swift b/FreeAPS/Sources/Services/Network/NightscoutAPI.swift index b218f2de3..ecf9b7b8d 100644 --- a/FreeAPS/Sources/Services/Network/NightscoutAPI.swift +++ b/FreeAPS/Sources/Services/Network/NightscoutAPI.swift @@ -112,7 +112,7 @@ extension NightscoutAPI { ), URLQueryItem( name: "find[enteredBy][$ne]", - value: NigtscoutTreatment.local.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) + value: NightscoutTreatment.local.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ) ] if let date = sinceDate { @@ -213,7 +213,7 @@ extension NightscoutAPI { ), URLQueryItem( name: "find[enteredBy][$ne]", - value: NigtscoutTreatment.local.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) + value: NightscoutTreatment.local.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ), URLQueryItem(name: "find[duration][$exists]", value: "true") ] @@ -278,7 +278,7 @@ extension NightscoutAPI { .eraseToAnyPublisher() } - func uploadTreatments(_ treatments: [NigtscoutTreatment]) -> AnyPublisher { + func uploadTreatments(_ treatments: [NightscoutTreatment]) -> AnyPublisher { var components = URLComponents() components.scheme = url.scheme components.host = url.host diff --git a/FreeAPS/Sources/Services/Network/NightscoutManager.swift b/FreeAPS/Sources/Services/Network/NightscoutManager.swift index 4b3107a1f..2e0f2ecaf 100644 --- a/FreeAPS/Sources/Services/Network/NightscoutManager.swift +++ b/FreeAPS/Sources/Services/Network/NightscoutManager.swift @@ -369,7 +369,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable { var status: NightscoutStatus status = NightscoutStatus( - device: NigtscoutTreatment.local, + device: NightscoutTreatment.local, openaps: openapsStatus, pump: pump, uploader: uploader @@ -398,11 +398,11 @@ final class BaseNightscoutManager: NightscoutManager, Injectable { } 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), uploadedPodAge.last?.createdAt == nil || podAge != uploadedPodAge.last!.createdAt! { - let siteTreatment = NigtscoutTreatment( + let siteTreatment = NightscoutTreatment( duration: nil, rawDuration: nil, rawRate: nil, @@ -410,7 +410,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable { rate: nil, eventType: .nsSiteChange, createdAt: podAge, - enteredBy: NigtscoutTreatment.local, + enteredBy: NightscoutTreatment.local, bolus: nil, insulin: nil, notes: nil, @@ -528,7 +528,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable { startDate: now, mills: Int(now.timeIntervalSince1970) * 1000, units: nsUnits, - enteredBy: NigtscoutTreatment.local, + enteredBy: NightscoutTreatment.local, store: [defaultProfile: ps] ) @@ -578,7 +578,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable { } private func uploadPumpHistory() { - uploadTreatments(pumpHistoryStorage.nightscoutTretmentsNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedPumphistory) + uploadTreatments(pumpHistoryStorage.nightscoutTreatmentsNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedPumphistory) } 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 { return }