Jonas Björkert пре 1 година
родитељ
комит
e1d5e64386

+ 8 - 0
LoopFollow.xcodeproj/project.pbxproj

@@ -141,6 +141,8 @@
 		DDB0AF522BB1A8BE00AFA48B /* BuildDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB0AF512BB1A8BE00AFA48B /* BuildDetails.swift */; };
 		DDB0AF552BB1B24A00AFA48B /* BuildDetails.plist in Resources */ = {isa = PBXBuildFile; fileRef = DDB0AF542BB1B24A00AFA48B /* BuildDetails.plist */; };
 		DDBE3ABD2CB5A961006B37DC /* OverrideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDBE3ABC2CB5A961006B37DC /* OverrideView.swift */; };
+		DDC6CA3D2DD7C6090060EE25 /* TemporaryCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA3C2DD7C6090060EE25 /* TemporaryCondition.swift */; };
+		DDC6CA3F2DD7C6340060EE25 /* TemporaryAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA3E2DD7C6340060EE25 /* TemporaryAlarmEditor.swift */; };
 		DDC7E5152DBCFA7900EB1127 /* SnoozerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5132DBCE1B900EB1127 /* SnoozerViewController.swift */; };
 		DDC7E5162DBCFA7F00EB1127 /* SnoozerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5122DBCE1B900EB1127 /* SnoozerView.swift */; };
 		DDC7E5382DBD887400EB1127 /* isOnPhoneCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5372DBD887400EB1127 /* isOnPhoneCall.swift */; };
@@ -485,6 +487,8 @@
 		DDB0AF512BB1A8BE00AFA48B /* BuildDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildDetails.swift; sourceTree = "<group>"; };
 		DDB0AF542BB1B24A00AFA48B /* BuildDetails.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = BuildDetails.plist; sourceTree = "<group>"; };
 		DDBE3ABC2CB5A961006B37DC /* OverrideView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideView.swift; sourceTree = "<group>"; };
+		DDC6CA3C2DD7C6090060EE25 /* TemporaryCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemporaryCondition.swift; sourceTree = "<group>"; };
+		DDC6CA3E2DD7C6340060EE25 /* TemporaryAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemporaryAlarmEditor.swift; sourceTree = "<group>"; };
 		DDC7E5122DBCE1B900EB1127 /* SnoozerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnoozerView.swift; sourceTree = "<group>"; };
 		DDC7E5132DBCE1B900EB1127 /* SnoozerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnoozerViewController.swift; sourceTree = "<group>"; };
 		DDC7E5372DBD887400EB1127 /* isOnPhoneCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = isOnPhoneCall.swift; sourceTree = "<group>"; };
@@ -730,6 +734,7 @@
 		DD02475A2DB2E8CE00FCADF6 /* AlarmCondition */ = {
 			isa = PBXGroup;
 			children = (
+				DDC6CA3C2DD7C6090060EE25 /* TemporaryCondition.swift */,
 				DD7F4C1E2DD6648B00D449E9 /* FastRiseCondition.swift */,
 				DD7F4C1A2DD6501D00D449E9 /* COBCondition.swift */,
 				DD7F4C162DD63FA700D449E9 /* RecBolusCondition.swift */,
@@ -999,6 +1004,7 @@
 		DDC7E53F2DBD8A1600EB1127 /* Editors */ = {
 			isa = PBXGroup;
 			children = (
+				DDC6CA3E2DD7C6340060EE25 /* TemporaryAlarmEditor.swift */,
 				DD7F4C202DD66BB100D449E9 /* FastRiseAlarmEditor.swift */,
 				DD7F4C1C2DD650D500D449E9 /* COBAlarmEditor.swift */,
 				DD7F4C182DD63FD500D449E9 /* RecBolusAlarmEditor.swift */,
@@ -1744,6 +1750,7 @@
 				DD0247712DB4337700FCADF6 /* BuildExpireCondition.swift in Sources */,
 				DDF6999B2C5AA32E0058A8D9 /* TempTargetPreset.swift in Sources */,
 				DD7F4C0F2DD51EC200D449E9 /* TempTargetStartCondition.swift in Sources */,
+				DDC6CA3D2DD7C6090060EE25 /* TemporaryCondition.swift in Sources */,
 				DD9ACA0E2D340BFF00415D8A /* AlarmTask.swift in Sources */,
 				DDCF9A822D85FD15004DF4DD /* AlarmType.swift in Sources */,
 				DD7FFAFD2A72953000C3A304 /* EKEventStore+Extensions.swift in Sources */,
@@ -1835,6 +1842,7 @@
 				DD4878052C7B2C970048F05C /* Storage.swift in Sources */,
 				DD493AE12ACF22FE009A6922 /* Profile.swift in Sources */,
 				DD493ADF2ACF22BB009A6922 /* SAge.swift in Sources */,
+				DDC6CA3F2DD7C6340060EE25 /* TemporaryAlarmEditor.swift in Sources */,
 				DDF699992C5AA3060058A8D9 /* TempTargetPresetManager.swift in Sources */,
 				DD0C0C722C4B000800DBADDF /* TrioNightscoutRemoteView.swift in Sources */,
 				DD493ADB2ACF21A3009A6922 /* Bolus.swift in Sources */,

+ 6 - 2
LoopFollow/Alarm/Alarm.swift

@@ -272,6 +272,8 @@ struct Alarm: Identifiable, Codable, Equatable {
         case .tempTargetEnd:
             soundFile = .alertToneBusy
             repeatSoundOption = .never
+        case .temporary:
+            soundFile = .indeed
         }
     }
 }
@@ -286,14 +288,14 @@ extension AlarmType {
 
     var group: Group {
         switch self {
-        case .low, .high, .fastDrop, .fastRise, .missedReading:
+        case .low, .high, .fastDrop, .fastRise, .missedReading, .temporary:
             return .glucose
         case .iob, .bolus, .cob, .missedBolus, .recBolus:
             return .insulin
         case .battery, .batteryDrop, .pump, .pumpChange,
              .sensorChange, .notLooping, .buildExpire:
             return .device
-        default:
+        case .overrideStart, .overrideEnd, .tempTargetStart, .tempTargetEnd:
             return .other
         }
     }
@@ -320,6 +322,7 @@ extension AlarmType {
         case .overrideEnd: return "stop.circle"
         case .tempTargetStart: return "flag"
         case .tempTargetEnd: return "flag.slash"
+        case .temporary: return "bell"
         }
     }
 
@@ -346,6 +349,7 @@ extension AlarmType {
         case .overrideEnd: return "Override ended."
         case .tempTargetStart: return "Temp target started."
         case .tempTargetEnd: return "Temp target ended."
+        case .temporary: return "One-time BG limit alert."
         }
     }
 }

+ 24 - 0
LoopFollow/Alarm/AlarmCondition/TemporaryCondition.swift

@@ -0,0 +1,24 @@
+//
+//  TemporaryCondition.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2025-05-16.
+//  Copyright © 2025 Jon Fawcett. All rights reserved.
+//
+
+import Foundation
+
+/// A throw-away, single-fire BG-limit alarm.
+struct TemporaryCondition: AlarmCondition {
+    static let type: AlarmType = .temporary
+    init() {}
+
+    func evaluate(alarm: Alarm, data _: AlarmData) -> Bool {
+        // Needs at least ONE limit
+        guard alarm.belowBG != nil || alarm.aboveBG != nil else { return false }
+
+        // BG-limit checks are handled in shouldFire → passesBGLimits.
+        // If we get here, the limits are satisfied ⇒ fire.
+        return true
+    }
+}

+ 2 - 0
LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift

@@ -67,6 +67,8 @@ struct AlarmEditor: View {
             COBAlarmEditor(alarm: $alarm)
         case .fastRise:
             FastRiseAlarmEditor(alarm: $alarm)
+        case .temporary:
+            TemporaryAlarmEditor(alarm: $alarm)
         /* TODO: add other condition types here */
         default:
             Text("No editor for \(alarm.type.rawValue)")

+ 56 - 0
LoopFollow/Alarm/AlarmEditing/Editors/TemporaryAlarmEditor.swift

@@ -0,0 +1,56 @@
+//
+//  TemporaryAlarmEditor.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2025-05-16.
+//  Copyright © 2025 Jon Fawcett. All rights reserved.
+//
+
+import SwiftUI
+
+struct TemporaryAlarmEditor: View {
+    @Binding var alarm: Alarm
+
+    // Shared BG range
+    private let bgRange: ClosedRange<Double> = 40 ... 300
+
+    var body: some View {
+        Form {
+            InfoBanner(
+                text: "This alert fires once when glucose crosses either of the limits you set below, and then disables itself.",
+                alarmType: alarm.type
+            )
+
+            AlarmGeneralSection(alarm: $alarm)
+
+            AlarmBGLimitSection(
+                header: "Low Limit",
+                footer: "Alert if BG is equal to or below this value.",
+                toggleText: "Enable low limit",
+                pickerTitle: "≤ BG",
+                range: bgRange,
+                value: $alarm.belowBG
+            )
+
+            AlarmBGLimitSection(
+                header: "High Limit",
+                footer: "Alert if BG is equal to or above this value.",
+                toggleText: "Enable high limit",
+                pickerTitle: "≥ BG",
+                range: bgRange,
+                value: $alarm.aboveBG
+            )
+
+            // Validation: ensure at least one limit is on
+            if alarm.belowBG == nil && alarm.aboveBG == nil {
+                Text("⚠️ Please enable at least one limit.")
+                    .foregroundColor(.red)
+            }
+
+            AlarmActiveSection(alarm: $alarm)
+            AlarmAudioSection(alarm: $alarm)
+            AlarmSnoozeSection(alarm: $alarm, range: 5 ... 60, step: 5)
+        }
+        .navigationTitle(alarm.type.rawValue)
+    }
+}

+ 10 - 0
LoopFollow/Alarm/AlarmManager.swift

@@ -30,6 +30,7 @@ class AlarmManager {
             COBCondition.self,
             MissedReadingCondition.self,
             FastRiseCondition.self,
+            TemporaryCondition.self,
             // TODO: add other condition types here
         ]
     ) {
@@ -137,6 +138,15 @@ class AlarmManager {
             {
                 lastBGAlarmTime = latestDate
             }
+
+            if alarm.type == .temporary {
+                // turn it off and persist
+                var list = Storage.shared.alarms.value
+                if let idx = list.firstIndex(where: { $0.id == alarm.id }) {
+                    list[idx].isEnabled = false
+                    Storage.shared.alarms.value = list
+                }
+            }
             break
         }
     }

+ 2 - 2
LoopFollow/Alarm/AlarmType/AlarmType+timeUnit.swift

@@ -18,7 +18,7 @@ extension AlarmType {
              .missedReading, .notLooping, .missedBolus,
              .bolus, .recBolus,
              .overrideStart, .overrideEnd, .tempTargetStart,
-             .tempTargetEnd:
+             .tempTargetEnd, .temporary:
             return .minute
         case .battery, .batteryDrop, .sensorChange, .pumpChange, .cob, .iob,
              .pump:
@@ -42,7 +42,7 @@ enum TimeUnit {
     /// A user-facing label
     var label: String {
         switch self {
-        case .minute: return "min" // Changed from minutes to save ui space
+        case .minute: return "min"
         case .hour: return "hours"
         case .day: return "days"
         }

+ 2 - 1
LoopFollow/Alarm/AlarmType/AlarmType.swift

@@ -11,6 +11,7 @@ import Foundation
 /// Categorizes alarms into distinct types, prioritized in the order they appear here.
 /// Multiple user-defined alarms may share the same type but differ in configuration.
 enum AlarmType: String, CaseIterable, Codable {
+    case temporary = "Temporary Alert"
     case iob = "IOB Alert"
     case bolus = "Bolus Alert"
     case cob = "COB Alert"
@@ -45,7 +46,7 @@ extension AlarmType {
     /// or its rate of change.
     var isBGBased: Bool {
         switch self {
-        case .low, .high, .fastDrop, .fastRise, .missedReading:
+        case .low, .high, .fastDrop, .fastRise, .missedReading, .temporary:
             return true
         default:
             return false

+ 1 - 0
LoopFollow/Alarm/GlucoseValue.swift

@@ -8,6 +8,7 @@
 
 import Foundation
 
+// Make use of this more clean glucose struct in more places
 struct GlucoseValue: Codable {
     let sgv: Int
     let date: Date