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

+ 18 - 2
LoopFollow.xcodeproj/project.pbxproj

@@ -163,6 +163,8 @@
 		DDC7E5CF2DC77C2000EB1127 /* SnoozerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5CE2DC77C2000EB1127 /* SnoozerViewModel.swift */; };
 		DDCC3A4B2DDBB5E4006F1C10 /* BatteryCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCC3A4A2DDBB5E4006F1C10 /* BatteryCondition.swift */; };
 		DDCC3A4D2DDBB77C006F1C10 /* BatteryAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCC3A4C2DDBB77C006F1C10 /* BatteryAlarmEditor.swift */; };
+		DDCC3A4F2DDC5B54006F1C10 /* BatteryDropCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCC3A4E2DDC5B54006F1C10 /* BatteryDropCondition.swift */; };
+		DDCC3A542DDC5D62006F1C10 /* BatteryDropAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCC3A532DDC5D62006F1C10 /* BatteryDropAlarmEditor.swift */; };
 		DDCF979424C0D380002C9752 /* UIViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF979324C0D380002C9752 /* UIViewExtension.swift */; };
 		DDCF979624C1443C002C9752 /* GeneralSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF979524C1443C002C9752 /* GeneralSettingsViewController.swift */; };
 		DDCF979824C1489C002C9752 /* GraphSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF979724C1489C002C9752 /* GraphSettingsViewController.swift */; };
@@ -520,6 +522,8 @@
 		DDC7E5CE2DC77C2000EB1127 /* SnoozerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnoozerViewModel.swift; sourceTree = "<group>"; };
 		DDCC3A4A2DDBB5E4006F1C10 /* BatteryCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryCondition.swift; sourceTree = "<group>"; };
 		DDCC3A4C2DDBB77C006F1C10 /* BatteryAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryAlarmEditor.swift; sourceTree = "<group>"; };
+		DDCC3A4E2DDC5B54006F1C10 /* BatteryDropCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryDropCondition.swift; sourceTree = "<group>"; };
+		DDCC3A532DDC5D62006F1C10 /* BatteryDropAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryDropAlarmEditor.swift; sourceTree = "<group>"; };
 		DDCF979324C0D380002C9752 /* UIViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewExtension.swift; sourceTree = "<group>"; };
 		DDCF979524C1443C002C9752 /* GeneralSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsViewController.swift; sourceTree = "<group>"; };
 		DDCF979724C1489C002C9752 /* GraphSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphSettingsViewController.swift; sourceTree = "<group>"; };
@@ -756,6 +760,7 @@
 		DD02475A2DB2E8CE00FCADF6 /* AlarmCondition */ = {
 			isa = PBXGroup;
 			children = (
+				DDCC3A4E2DDC5B54006F1C10 /* BatteryDropCondition.swift */,
 				DDCC3A4A2DDBB5E4006F1C10 /* BatteryCondition.swift */,
 				DDB9FC7A2DDB573F00EFAA76 /* IOBCondition.swift */,
 				DDC6CA482DD8E47A0060EE25 /* PumpVolumeCondition.swift */,
@@ -1031,6 +1036,7 @@
 		DDC7E53F2DBD8A1600EB1127 /* Editors */ = {
 			isa = PBXGroup;
 			children = (
+				DDCC3A532DDC5D62006F1C10 /* BatteryDropAlarmEditor.swift */,
 				DDCC3A4C2DDBB77C006F1C10 /* BatteryAlarmEditor.swift */,
 				DDB9FC7C2DDB575300EFAA76 /* IOBAlarmEditor.swift */,
 				DDC6CA4A2DD8E4960060EE25 /* PumpVolumeAlarmEditor.swift */,
@@ -1076,12 +1082,20 @@
 			path = Storage;
 			sourceTree = "<group>";
 		};
-		DDCF9A7E2D85FCE6004DF4DD /* Alarm */ = {
+		DDCC3A502DDC5BD4006F1C10 /* DataStructs */ = {
 			isa = PBXGroup;
 			children = (
 				DDB9FC7E2DDB584500EFAA76 /* BolusEntry.swift */,
-				DDC6CA3B2DD7B9050060EE25 /* AlarmType */,
 				DD5DA27B2DC930D6003D44FC /* GlucoseValue.swift */,
+			);
+			path = DataStructs;
+			sourceTree = "<group>";
+		};
+		DDCF9A7E2D85FCE6004DF4DD /* Alarm */ = {
+			isa = PBXGroup;
+			children = (
+				DDCC3A502DDC5BD4006F1C10 /* DataStructs */,
+				DDC6CA3B2DD7B9050060EE25 /* AlarmType */,
 				DD4AFB602DB68BBC00BB593F /* AlarmListView.swift */,
 				DDC7E5412DBD8A1600EB1127 /* AlarmEditing */,
 				DD4AFB482DB576C200BB593F /* AlarmSettingsView.swift */,
@@ -1811,6 +1825,7 @@
 				FC16A97A24996673003D6245 /* NightScout.swift in Sources */,
 				DD07B5C929E2F9C400C6A635 /* NightscoutUtils.swift in Sources */,
 				DDCF9A8C2D86005E004DF4DD /* AlarmManager.swift in Sources */,
+				DDCC3A4F2DDC5B54006F1C10 /* BatteryDropCondition.swift in Sources */,
 				FCC6886924898FB100A0279D /* UserDefaultsValueGroups.swift in Sources */,
 				DDF2C0122BEFB733007A20E6 /* AppVersionManager.swift in Sources */,
 				DDB9FC7B2DDB573F00EFAA76 /* IOBCondition.swift in Sources */,
@@ -1884,6 +1899,7 @@
 				DD0C0C722C4B000800DBADDF /* TrioNightscoutRemoteView.swift in Sources */,
 				DD493ADB2ACF21A3009A6922 /* Bolus.swift in Sources */,
 				DDF9676E2AD08C6E00C5EB95 /* SiteChange.swift in Sources */,
+				DDCC3A542DDC5D62006F1C10 /* BatteryDropAlarmEditor.swift in Sources */,
 				DD13BC772C3FD64E0062313B /* InfoData.swift in Sources */,
 				DD13BC752C3FD6210062313B /* InfoType.swift in Sources */,
 				DDCC3A4D2DDBB77C006F1C10 /* BatteryAlarmEditor.swift in Sources */,

+ 1 - 1
LoopFollow/Alarm/AlarmCondition/BatteryCondition.swift

@@ -1,6 +1,6 @@
 // LoopFollow
 // BatteryCondition.swift
-// Created by Jonas Björkert on 2025-05-19.
+// Created by Jonas Björkert on 2025-05-20.
 
 import Foundation
 

+ 36 - 0
LoopFollow/Alarm/AlarmCondition/BatteryDropCondition.swift

@@ -0,0 +1,36 @@
+// LoopFollow
+// BatteryDropCondition.swift
+// Created by Jonas Björkert on 2025-05-20.
+
+import Foundation
+
+/// Fires when the phone-battery **falls by ≥ Δ % within N minutes**.
+///
+/// * `alarm.delta`   ➜ percentage drop (e.g. 5 %)
+/// * `alarm.monitoringWindow` ➜ window in minutes (e.g. 15)
+/// * Uses `data.batteryHistory`, which must be sorted **oldest → newest**.
+struct BatteryDropCondition: AlarmCondition {
+    static let type: AlarmType = .batteryDrop
+    init() {}
+
+    func evaluate(alarm: Alarm, data: AlarmData, now _: Date) -> Bool {
+        // ───── 0. sanity ───────────────────────────────────────────────
+        guard
+            let drop = alarm.delta, drop > 0,
+            let minutes = alarm.monitoringWindow, minutes > 0,
+            let latest = data.batteryHistory.last
+        else { return false }
+
+        // find the sample *closest* to “minutes” ago
+        let target = latest.timestamp.addingTimeInterval(-Double(minutes) * 60)
+
+        guard let earlier = data.batteryHistory.min(by: {
+            abs($0.timestamp.timeIntervalSince(target)) < abs($1.timestamp.timeIntervalSince(target))
+        }) else { return false }
+
+        // ignore if the earlier level was 100 % (false drop when just unplugged)
+        guard earlier.batteryLevel < 100 else { return false }
+
+        return (earlier.batteryLevel - latest.batteryLevel) >= drop
+    }
+}

+ 1 - 4
LoopFollow/Alarm/AlarmData.swift

@@ -21,12 +21,9 @@ struct AlarmData: Codable {
     let IOB: Double?
     let recentBoluses: [BolusEntry]
     let latestBattery: Double?
+    let batteryHistory: [DataStructs.batteryStruct]
 }
 
 /*
- //    let iob: Double?
- //    let latestBoluses: [BolusEntry]
- //    let batteryLevel: Double?
  //    let latestCarbs: [CarbEntry]
- //    let pumpVolume: Double?
  */

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

@@ -76,6 +76,8 @@ struct AlarmEditor: View {
             IOBAlarmEditor(alarm: $alarm)
         case .battery:
             BatteryAlarmEditor(alarm: $alarm)
+        case .batteryDrop:
+            BatteryDropAlarmEditor(alarm: $alarm)
         /* TODO: add other condition types here */
         default:
             Text("No editor for \(alarm.type.rawValue)")

+ 1 - 1
LoopFollow/Alarm/AlarmEditing/Editors/BatteryAlarmEditor.swift

@@ -1,6 +1,6 @@
 // LoopFollow
 // BatteryAlarmEditor.swift
-// Created by Jonas Björkert on 2025-05-19.
+// Created by Jonas Björkert on 2025-05-17.
 
 import SwiftUI
 

+ 53 - 0
LoopFollow/Alarm/AlarmEditing/Editors/BatteryDropAlarmEditor.swift

@@ -0,0 +1,53 @@
+// LoopFollow
+// BatteryDropAlarmEditor.swift
+// Created by Jonas Björkert on 2025-05-20.
+
+import SwiftUI
+
+struct BatteryDropAlarmEditor: View {
+    @Binding var alarm: Alarm
+
+    var body: some View {
+        Form {
+            InfoBanner(
+                text: "Alerts when the phone battery falls by a specified "
+                    + "percentage within a set time window.",
+                alarmType: alarm.type
+            )
+
+            AlarmGeneralSection(alarm: $alarm)
+
+            AlarmStepperSection(
+                header: "Drop Amount",
+                footer: "Trigger when charge falls by at least this much.",
+                title: "Δ %",
+                range: 5 ... 100,
+                step: 5,
+                unitLabel: "%",
+                value: Binding(
+                    get: { alarm.delta ?? 10 },
+                    set: { alarm.delta = $0 }
+                )
+            )
+
+            AlarmStepperSection(
+                header: "Time Window",
+                footer: "How far back to look for that drop.",
+                title: "Minutes",
+                range: 5 ... 30,
+                step: 5,
+                value: Binding(
+                    get: { Double(alarm.monitoringWindow ?? 15) },
+                    set: { alarm.monitoringWindow = Int($0) }
+                )
+            )
+
+            AlarmActiveSection(alarm: $alarm)
+            AlarmAudioSection(alarm: $alarm)
+            AlarmSnoozeSection(alarm: $alarm,
+                               range: 1 ... 24,
+                               step: 1)
+        }
+        .navigationTitle(alarm.type.rawValue)
+    }
+}

+ 1 - 1
LoopFollow/Alarm/AlarmManager.swift

@@ -32,7 +32,7 @@ class AlarmManager {
             PumpVolumeCondition.self,
             IOBCondition.self,
             BatteryCondition.self,
-
+            BatteryDropCondition.self,
             // TODO: add other condition types here
         ]
     ) {

LoopFollow/Alarm/BolusEntry.swift → LoopFollow/Alarm/DataStructs/BolusEntry.swift


LoopFollow/Alarm/GlucoseValue.swift → LoopFollow/Alarm/DataStructs/GlucoseValue.swift


+ 2 - 1
LoopFollow/Task/AlarmTask.swift

@@ -47,7 +47,8 @@ extension MainViewController {
                 latestPumpVolume: latestPumpVol,
                 IOB: self.latestIOB?.value,
                 recentBoluses: bolusEntries,
-                latestBattery: latestBattery
+                latestBattery: latestBattery,
+                batteryHistory: self.deviceBatteryData
             )
 
             let finalAlarmData: AlarmData