Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ struct LoopAPNSCarbsView: View {

@Environment(\.presentationMode) var presentationMode
@ObservedObject private var quickPickMeals = QuickPickMealsManager.shared
@ObservedObject private var maxFutureCarbHours = Storage.shared.loopMaxFutureCarbHours
@State private var carbsAmount = HKQuantity(unit: .gram(), doubleValue: 0.0)
@State private var absorptionHours = 3
@State private var absorptionMinutes = 0
Expand All @@ -25,12 +26,15 @@ struct LoopAPNSCarbsView: View {
private let otpPeriod: TimeInterval = 30
private let timeAdjustmentStepMinutes = 5
private let maxPastHours = 12
private let maxFutureHours = 1
private let minAllowedAbsorptionTime = 0.5
private let maxAllowedAbsorptionTime = 8.0
private let lollipopStandardAbsorption: AbsorptionPreset = (0, 30)
private let tacoStandardAbsorption: AbsorptionPreset = (3, 0)
private let pizzaStandardAbsorption: AbsorptionPreset = (5, 0)
private var maxFutureHours: Int {
maxFutureCarbHours.value
}

private var otpTimer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

@FocusState private var carbsFieldIsFocused: Bool
Expand Down Expand Up @@ -516,7 +520,7 @@ struct LoopAPNSCarbsView: View {
}

if consumedDate > latestAcceptedDate {
alertMessage = "Time must be within the next \(maxFutureHours) hour"
alertMessage = "Time must be within the next \(maxFutureHours) \(maxFutureHours == 1 ? "hour" : "hours")"
alertType = .error
showAlert = true
return
Expand Down
5 changes: 4 additions & 1 deletion LoopFollow/Remote/LoopAPNS/LoopCarbEditSheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ struct LoopCarbEditSheet: View {
@FocusState private var carbsFocused: Bool

private let maxPastHours = 12
private let maxFutureHours = 1
private var maxFutureHours: Int {
Storage.shared.loopMaxFutureCarbHours.value
}

private let minAbsorptionHours = 0.5
private let maxAbsorptionHours = 8

Expand Down
14 changes: 14 additions & 0 deletions LoopFollow/Remote/Settings/RemoteSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ struct RemoteSettingsView: View {
@ObservedObject private var device = Storage.shared.device
@ObservedObject var bolusIncrement = Storage.shared.bolusIncrement
@ObservedObject private var watchRemoteCommands = Storage.shared.watchRemoteCommands
@ObservedObject private var loopMaxFutureCarbHours = Storage.shared.loopMaxFutureCarbHours

@State private var showAlert: Bool = false
@State private var alertType: AlertType? = nil
Expand Down Expand Up @@ -90,6 +91,19 @@ struct RemoteSettingsView: View {
}
}

if viewModel.remoteType == .loopAPNS {
Section(
header: Text("Meal Settings"),
footer: Text("How far ahead carbs can be entered. Loop accepts up to 1 hr; use 4 hr if Loop is built with the future_carbs_4h customization.")
) {
Picker("Max Future Carbs", selection: $loopMaxFutureCarbHours.value) {
ForEach(1 ... 4, id: \.self) { hours in
Text("\(hours) hr").tag(hours)
}
}
}
}

// MARK: - Guardrails Section (shown for both TRC and Loop)

if viewModel.remoteType == .trc || viewModel.remoteType == .loopAPNS {
Expand Down
5 changes: 5 additions & 0 deletions LoopFollow/Settings/ImportExport/ExportableSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ struct RemoteSettingsExport: Codable {
let mealWithFatProtein: Bool
let productionEnvironment: Bool
let loopAPNSQrCodeURL: String
let loopMaxFutureCarbHours: Int?
let device: String

static func fromCurrentStorage() -> RemoteSettingsExport {
Expand All @@ -262,6 +263,7 @@ struct RemoteSettingsExport: Codable {
mealWithFatProtein: storage.mealWithFatProtein.value,
productionEnvironment: storage.productionEnvironment.value,
loopAPNSQrCodeURL: storage.loopAPNSQrCodeURL.value,
loopMaxFutureCarbHours: storage.loopMaxFutureCarbHours.value,
device: storage.device.value
)
}
Expand All @@ -283,6 +285,9 @@ struct RemoteSettingsExport: Codable {
storage.mealWithFatProtein.value = mealWithFatProtein
storage.productionEnvironment.value = productionEnvironment
storage.loopAPNSQrCodeURL.value = loopAPNSQrCodeURL
if let loopMaxFutureCarbHours {
storage.loopMaxFutureCarbHours.value = loopMaxFutureCarbHours
}

// Set device temporarily from import (will be overridden by Nightscout connection)
if !device.isEmpty {
Expand Down
1 change: 1 addition & 0 deletions LoopFollow/Storage/Storage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ class Storage {
var treatmentsPosition = StorageValue<TabPosition>(key: "treatmentsPosition", defaultValue: .menu)

var loopAPNSQrCodeURL = StorageValue<String>(key: "loopAPNSQrCodeURL", defaultValue: "")
var loopMaxFutureCarbHours = StorageValue<Int>(key: "loopMaxFutureCarbHours", defaultValue: 1)

var bolusIncrement = SecureStorageValue<HKQuantity>(key: "bolusIncrement", defaultValue: HKQuantity(unit: .internationalUnit(), doubleValue: 0.05))
var bolusIncrementDetected = StorageValue<Bool>(key: "bolusIncrementDetected", defaultValue: false)
Expand Down
4 changes: 2 additions & 2 deletions LoopFollow/Treatments/LoopCarbTreatment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ struct LoopCarbTreatment: Codable, Equatable {
absorptionMinutes.map { $0 / 60 }
}

func isWithinEditWindow(now: Date = Date()) -> Bool {
func isWithinEditWindow(now: Date = Date(), maxFutureHours: Int = Storage.shared.loopMaxFutureCarbHours.value) -> Bool {
let age = now.timeIntervalSince1970 - date
return age <= Self.editWindow && age >= -3600
return age <= Self.editWindow && age >= -TimeInterval(maxFutureHours) * 3600
}

/// Loop APNS targets a Loop device, whatever commands that build advertises.
Expand Down
4 changes: 3 additions & 1 deletion LoopFollow/Treatments/TreatmentsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -687,7 +687,9 @@ struct TreatmentDetailView: View {
}

private func loopRemoteActionsFooter(withinWindow: Bool) -> Text? {
withinWindow ? nil : Text("Carb entries can be changed remotely for 23 hours.")
guard !withinWindow else { return nil }
let futureHours = Storage.shared.loopMaxFutureCarbHours.value
return Text("Carb entries can be changed remotely up to 23 hours after and \(futureHours) \(futureHours == 1 ? "hour" : "hours") before their time.")
}

@ViewBuilder
Expand Down
12 changes: 7 additions & 5 deletions Tests/LoopCarbTreatmentTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,18 @@ struct LoopCarbTreatmentTests {
#expect(LoopCarbTreatment(nightscoutEntry: entry(carbs: 0), date: 0) == nil)
}

@Test("edit window is 23 h back and 1 h ahead")
@Test("edit window is 23 h back and the max future carbs setting ahead")
func window() {
let now = Date(timeIntervalSince1970: 1_700_000_000)
func carb(offsetHours: Double) -> LoopCarbTreatment? {
LoopCarbTreatment(nightscoutEntry: entry(), date: now.timeIntervalSince1970 + offsetHours * 3600)
}
#expect(carb(offsetHours: -22)?.isWithinEditWindow(now: now) == true)
#expect(carb(offsetHours: -24)?.isWithinEditWindow(now: now) == false)
#expect(carb(offsetHours: 0.5)?.isWithinEditWindow(now: now) == true)
#expect(carb(offsetHours: 2)?.isWithinEditWindow(now: now) == false)
#expect(carb(offsetHours: -22)?.isWithinEditWindow(now: now, maxFutureHours: 1) == true)
#expect(carb(offsetHours: -24)?.isWithinEditWindow(now: now, maxFutureHours: 1) == false)
#expect(carb(offsetHours: 0.5)?.isWithinEditWindow(now: now, maxFutureHours: 1) == true)
#expect(carb(offsetHours: 2)?.isWithinEditWindow(now: now, maxFutureHours: 1) == false)
#expect(carb(offsetHours: 3.8)?.isWithinEditWindow(now: now, maxFutureHours: 4) == true)
#expect(carb(offsetHours: 4.5)?.isWithinEditWindow(now: now, maxFutureHours: 4) == false)
}

@Test("actions need Loop APNS, a Loop device and both commands advertised")
Expand Down