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

+ 8 - 0
LoopFollow.xcodeproj/project.pbxproj

@@ -145,6 +145,8 @@
 		DDC6CA3F2DD7C6340060EE25 /* TemporaryAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA3E2DD7C6340060EE25 /* TemporaryAlarmEditor.swift */; };
 		DDC6CA412DD8CCCE0060EE25 /* SensorAgeAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA402DD8CCCE0060EE25 /* SensorAgeAlarmEditor.swift */; };
 		DDC6CA432DD8CED20060EE25 /* SensorAgeCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA422DD8CED20060EE25 /* SensorAgeCondition.swift */; };
+		DDC6CA452DD8D8E60060EE25 /* PumpChangeCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA442DD8D8E60060EE25 /* PumpChangeCondition.swift */; };
+		DDC6CA472DD8D9010060EE25 /* PumpChangeAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA462DD8D9010060EE25 /* PumpChangeAlarmEditor.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 */; };
@@ -493,6 +495,8 @@
 		DDC6CA3E2DD7C6340060EE25 /* TemporaryAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemporaryAlarmEditor.swift; sourceTree = "<group>"; };
 		DDC6CA402DD8CCCE0060EE25 /* SensorAgeAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SensorAgeAlarmEditor.swift; sourceTree = "<group>"; };
 		DDC6CA422DD8CED20060EE25 /* SensorAgeCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SensorAgeCondition.swift; sourceTree = "<group>"; };
+		DDC6CA442DD8D8E60060EE25 /* PumpChangeCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpChangeCondition.swift; sourceTree = "<group>"; };
+		DDC6CA462DD8D9010060EE25 /* PumpChangeAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpChangeAlarmEditor.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>"; };
@@ -738,6 +742,7 @@
 		DD02475A2DB2E8CE00FCADF6 /* AlarmCondition */ = {
 			isa = PBXGroup;
 			children = (
+				DDC6CA442DD8D8E60060EE25 /* PumpChangeCondition.swift */,
 				DDC6CA422DD8CED20060EE25 /* SensorAgeCondition.swift */,
 				DDC6CA3C2DD7C6090060EE25 /* TemporaryCondition.swift */,
 				DD7F4C1E2DD6648B00D449E9 /* FastRiseCondition.swift */,
@@ -1009,6 +1014,7 @@
 		DDC7E53F2DBD8A1600EB1127 /* Editors */ = {
 			isa = PBXGroup;
 			children = (
+				DDC6CA462DD8D9010060EE25 /* PumpChangeAlarmEditor.swift */,
 				DDC6CA402DD8CCCE0060EE25 /* SensorAgeAlarmEditor.swift */,
 				DDC6CA3E2DD7C6340060EE25 /* TemporaryAlarmEditor.swift */,
 				DD7F4C202DD66BB100D449E9 /* FastRiseAlarmEditor.swift */,
@@ -1850,6 +1856,7 @@
 				DD493ADF2ACF22BB009A6922 /* SAge.swift in Sources */,
 				DDC6CA3F2DD7C6340060EE25 /* TemporaryAlarmEditor.swift in Sources */,
 				DDF699992C5AA3060058A8D9 /* TempTargetPresetManager.swift in Sources */,
+				DDC6CA452DD8D8E60060EE25 /* PumpChangeCondition.swift in Sources */,
 				DD0C0C722C4B000800DBADDF /* TrioNightscoutRemoteView.swift in Sources */,
 				DD493ADB2ACF21A3009A6922 /* Bolus.swift in Sources */,
 				DDF9676E2AD08C6E00C5EB95 /* SiteChange.swift in Sources */,
@@ -1899,6 +1906,7 @@
 				DD91E4DD2BDEC3F8002D9E97 /* GlucoseConversion.swift in Sources */,
 				DD0650EF2DCE96FF004D3B41 /* HighBGCondition.swift in Sources */,
 				DDDF6F452D479AB100884336 /* LoopOverrideView.swift in Sources */,
+				DDC6CA472DD8D9010060EE25 /* PumpChangeAlarmEditor.swift in Sources */,
 				DD4878132C7B750D0048F05C /* TempTargetView.swift in Sources */,
 				DD0C0C682C48529400DBADDF /* Metric.swift in Sources */,
 				FCC6886D2489909D00A0279D /* AnyConvertible.swift in Sources */,

+ 38 - 0
LoopFollow/Alarm/AlarmCondition/PumpChangeCondition.swift

@@ -0,0 +1,38 @@
+// LoopFollow
+// PumpChangeCondition.swift
+// Created by Jonas Björkert on 2025-05-17.
+
+//
+//  PumpChangeCondition.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2025-05-17.
+//
+
+import Foundation
+
+/// Fires once when we are **≤ threshold hours** away from the Omnipod /
+/// cannula 3-day hard-stop.  Automatically disables itself after firing.
+struct PumpChangeCondition: AlarmCondition {
+    static let type: AlarmType = .pumpChange
+    init() {}
+
+    /// Pod lifetime = 3 days = 72 h
+    private let lifetime: TimeInterval = 3 * 24 * 60 * 60
+
+    func evaluate(alarm: Alarm, data: AlarmData) -> Bool {
+        // 0. sanity guards
+        guard let warnAheadHrs = alarm.threshold, warnAheadHrs > 0 else { return false }
+        guard let insertTS = data.pumpInsertTime else { return false }
+
+        // convert UNIX timestamp → Date
+        let insertedAt = Date(timeIntervalSince1970: insertTS)
+
+        // 1. compute “fire-at” moment
+        let expiry = insertedAt.addingTimeInterval(lifetime)
+        let trigger = expiry.addingTimeInterval(-warnAheadHrs * 3600)
+
+        // 2. nothing else to track – disableAfterFiring=true handles repeats
+        return Date() >= trigger
+    }
+}

+ 1 - 0
LoopFollow/Alarm/AlarmData.swift

@@ -16,6 +16,7 @@ struct AlarmData: Codable {
     let recBolus: Double?
     let COB: Double?
     let sageInsertTime: TimeInterval?
+    let pumpInsertTime: TimeInterval?
 }
 
 /*

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

@@ -75,6 +75,8 @@ struct AlarmEditor: View {
             TemporaryAlarmEditor(alarm: $alarm)
         case .sensorChange:
             SensorAgeAlarmEditor(alarm: $alarm)
+        case .pumpChange:
+            PumpChangeAlarmEditor(alarm: $alarm)
         /* TODO: add other condition types here */
         default:
             Text("No editor for \(alarm.type.rawValue)")

+ 48 - 0
LoopFollow/Alarm/AlarmEditing/Editors/PumpChangeAlarmEditor.swift

@@ -0,0 +1,48 @@
+// LoopFollow
+// PumpChangeAlarmEditor.swift
+// Created by Jonas Björkert on 2025-05-17.
+
+//
+//  PumpChangeAlarmEditor.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2025-05-17.
+//
+
+import SwiftUI
+
+struct PumpChangeAlarmEditor: View {
+    @Binding var alarm: Alarm
+
+    var body: some View {
+        Form {
+            InfoBanner(
+                text: "Alerts once when your pump / cannula is within the time "
+                    + "window you choose below (relative to the 3-day change "
+                    + "limit).  After it fires once it disables itself.",
+                alarmType: alarm.type
+            )
+
+            AlarmGeneralSection(alarm: $alarm)
+
+            AlarmStepperSection(
+                header: "Advance Notice",
+                footer: "How many hours before the 3-day limit the alert "
+                    + "should fire.  Set to 12 hours, for example, to get a "
+                    + "reminder half a day in advance.",
+                title: "Warn hours",
+                range: 1 ... 24,
+                step: 1,
+                value: Binding(
+                    get: { alarm.threshold ?? 12 },
+                    set: { alarm.threshold = $0 }
+                )
+            )
+
+            AlarmActiveSection(alarm: $alarm)
+            AlarmAudioSection(alarm: $alarm)
+            AlarmSnoozeSection(alarm: $alarm, range: 1 ... 24, step: 1)
+        }
+        .navigationTitle(alarm.type.rawValue)
+    }
+}

+ 1 - 0
LoopFollow/Alarm/AlarmManager.swift

@@ -28,6 +28,7 @@ class AlarmManager {
             FastRiseCondition.self,
             TemporaryCondition.self,
             SensorAgeCondition.self,
+            PumpChangeCondition.self,
             // TODO: add other condition types here
         ]
     ) {

+ 3 - 1
LoopFollow/Task/AlarmTask.swift

@@ -22,6 +22,7 @@ extension MainViewController {
             let recBolus = UserDefaultsRepository.deviceRecBolus.value
             let COB = self.latestCOB?.value
             let sensorInsertedAt = UserDefaultsRepository.alertSageInsertTime.value
+            let pumpInsertTime = UserDefaultsRepository.alertCageInsertTime.value
 
             let alarmData = AlarmData(
                 bgReadings: self.bgData
@@ -38,7 +39,8 @@ extension MainViewController {
                 latestTempTargetEnd: latestTempTargetEnd,
                 recBolus: recBolus,
                 COB: COB,
-                sageInsertTime: sensorInsertedAt
+                sageInsertTime: sensorInsertedAt,
+                pumpInsertTime: pumpInsertTime
             )
 
             let finalAlarmData: AlarmData