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

+ 8 - 0
LoopFollow.xcodeproj/project.pbxproj

@@ -143,6 +143,8 @@
 		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 */; };
+		DDC6CA412DD8CCCE0060EE25 /* SensorAgeAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA402DD8CCCE0060EE25 /* SensorAgeAlarmEditor.swift */; };
+		DDC6CA432DD8CED20060EE25 /* SensorAgeCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA422DD8CED20060EE25 /* SensorAgeCondition.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 */; };
@@ -489,6 +491,8 @@
 		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>"; };
+		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>"; };
 		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>"; };
@@ -734,6 +738,7 @@
 		DD02475A2DB2E8CE00FCADF6 /* AlarmCondition */ = {
 			isa = PBXGroup;
 			children = (
+				DDC6CA422DD8CED20060EE25 /* SensorAgeCondition.swift */,
 				DDC6CA3C2DD7C6090060EE25 /* TemporaryCondition.swift */,
 				DD7F4C1E2DD6648B00D449E9 /* FastRiseCondition.swift */,
 				DD7F4C1A2DD6501D00D449E9 /* COBCondition.swift */,
@@ -1004,6 +1009,7 @@
 		DDC7E53F2DBD8A1600EB1127 /* Editors */ = {
 			isa = PBXGroup;
 			children = (
+				DDC6CA402DD8CCCE0060EE25 /* SensorAgeAlarmEditor.swift */,
 				DDC6CA3E2DD7C6340060EE25 /* TemporaryAlarmEditor.swift */,
 				DD7F4C202DD66BB100D449E9 /* FastRiseAlarmEditor.swift */,
 				DD7F4C1C2DD650D500D449E9 /* COBAlarmEditor.swift */,
@@ -1896,6 +1902,7 @@
 				DD4878132C7B750D0048F05C /* TempTargetView.swift in Sources */,
 				DD0C0C682C48529400DBADDF /* Metric.swift in Sources */,
 				FCC6886D2489909D00A0279D /* AnyConvertible.swift in Sources */,
+				DDC6CA432DD8CED20060EE25 /* SensorAgeCondition.swift in Sources */,
 				DD7F4C212DD66BB200D449E9 /* FastRiseAlarmEditor.swift in Sources */,
 				FC97881A2485969B00A7906C /* SceneDelegate.swift in Sources */,
 				DD0C0C662C46E54C00DBADDF /* InfoDataSeparator.swift in Sources */,
@@ -1917,6 +1924,7 @@
 				FCD2A27D24C9D044009F7B7B /* Globals.swift in Sources */,
 				DD0C0C642C45A59400DBADDF /* HKUnit+Extensions.swift in Sources */,
 				DDCF979A24C14DB4002C9752 /* WatchSettingsViewController.swift in Sources */,
+				DDC6CA412DD8CCCE0060EE25 /* SensorAgeAlarmEditor.swift in Sources */,
 				DD493AE52ACF2383009A6922 /* Treatments.swift in Sources */,
 				DD7F4C112DD51ED900D449E9 /* TempTargetStartAlarmEditor.swift in Sources */,
 				DD50C7552D0862770057AE6F /* ContactImageUpdater.swift in Sources */,

+ 34 - 0
LoopFollow/Alarm/AlarmCondition/SensorAgeCondition.swift

@@ -0,0 +1,34 @@
+//
+//  SensorAgeCondition.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2025-05-17.
+//  Copyright © 2025 Jon Fawcett. All rights reserved.
+//
+
+import Foundation
+
+/// Fires once when we are **≤ threshold hours** away from the
+/// Dexcom 10-day hard-stop.  No repeats once triggered.
+struct SensorAgeCondition: AlarmCondition {
+    static let type: AlarmType = .sensorChange
+    init() {}
+
+    /// Dexcom hard-stop = 10 days = 240 h
+    private let lifetime: TimeInterval = 10 * 24 * 60 * 60
+
+    func evaluate(alarm: Alarm, data: AlarmData) -> Bool {
+        // 0. basic guards
+        guard let warnAheadHrs = alarm.threshold, warnAheadHrs > 0 else { return false }
+        guard let insertTS = data.sageInsertTime else { return false }
+
+        // convert UNIX timestamp to Date
+        let insertedAt = Date(timeIntervalSince1970: insertTS)
+
+        // 1. compute trigger moment
+        let expiry = insertedAt.addingTimeInterval(lifetime)
+        let trigger = expiry.addingTimeInterval(-warnAheadHrs * 3600)
+
+        return Date() >= trigger
+    }
+}

+ 1 - 0
LoopFollow/Alarm/AlarmData.swift

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

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

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

+ 45 - 0
LoopFollow/Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift

@@ -0,0 +1,45 @@
+//
+//  SensorAgeAlarmEditor.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2025-05-17.
+//  Copyright © 2025 Jon Fawcett. All rights reserved.
+//
+
+import SwiftUI
+
+struct SensorAgeAlarmEditor: View {
+    @Binding var alarm: Alarm
+
+    var body: some View {
+        Form {
+            InfoBanner(
+                text: "Warn me this many hours before the sensor’s 10-day change-over.",
+                alarmType: alarm.type
+            )
+
+            AlarmGeneralSection(alarm: $alarm)
+
+            AlarmStepperSection(
+                header: "Advance warning",
+                footer: "Number of hours before the 10-day mark that the alert " +
+                    "will fire.",
+                title: "Hours",
+                range: 1 ... 24,
+                step: 1,
+                unitLabel: "hours",
+                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

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

+ 4 - 0
LoopFollow/Alarm/AlarmType/AlarmType+SortDirection.swift

@@ -42,6 +42,10 @@ extension AlarmType {
             return (direction: .descending,
                     key: { $0.threshold })
 
+        case .sensorChange:
+            return (direction: .ascending,
+                    key: { $0.threshold })
+
         default:
             return nil
         }

+ 3 - 1
LoopFollow/Task/AlarmTask.swift

@@ -25,6 +25,7 @@ extension MainViewController {
             let latestTempTargetEnd = self.tempTargetGraphData.last?.endDate
             let recBolus = UserDefaultsRepository.deviceRecBolus.value
             let COB = self.latestCOB?.value
+            let sensorInsertedAt = UserDefaultsRepository.alertSageInsertTime.value
 
             let alarmData = AlarmData(
                 bgReadings: self.bgData
@@ -40,7 +41,8 @@ extension MainViewController {
                 latestTempTargetStart: latestTempTargetStart,
                 latestTempTargetEnd: latestTempTargetEnd,
                 recBolus: recBolus,
-                COB: COB
+                COB: COB,
+                sageInsertTime: sensorInsertedAt
             )
 
             let finalAlarmData: AlarmData