Jonas Björkert 1 год назад
Родитель
Сommit
e94054684d

+ 8 - 0
LoopFollow.xcodeproj/project.pbxproj

@@ -111,6 +111,8 @@
 		DD7F4C112DD51ED900D449E9 /* TempTargetStartAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C102DD51ED900D449E9 /* TempTargetStartAlarmEditor.swift */; };
 		DD7F4C132DD51FD500D449E9 /* TempTargetEndCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C122DD51FD500D449E9 /* TempTargetEndCondition.swift */; };
 		DD7F4C152DD51FEB00D449E9 /* TempTargetEndAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C142DD51FEB00D449E9 /* TempTargetEndAlarmEditor.swift */; };
+		DD7F4C172DD63FA700D449E9 /* RecBolusCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C162DD63FA700D449E9 /* RecBolusCondition.swift */; };
+		DD7F4C192DD63FD500D449E9 /* RecBolusAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C182DD63FD500D449E9 /* RecBolusAlarmEditor.swift */; };
 		DD7FFAFD2A72953000C3A304 /* EKEventStore+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */; };
 		DD85E9952D739CFE001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */; };
 		DD91E4DD2BDEC3F8002D9E97 /* GlucoseConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */; };
@@ -446,6 +448,8 @@
 		DD7F4C102DD51ED900D449E9 /* TempTargetStartAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetStartAlarmEditor.swift; sourceTree = "<group>"; };
 		DD7F4C122DD51FD500D449E9 /* TempTargetEndCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetEndCondition.swift; sourceTree = "<group>"; };
 		DD7F4C142DD51FEB00D449E9 /* TempTargetEndAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetEndAlarmEditor.swift; sourceTree = "<group>"; };
+		DD7F4C162DD63FA700D449E9 /* RecBolusCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecBolusCondition.swift; sourceTree = "<group>"; };
+		DD7F4C182DD63FD500D449E9 /* RecBolusAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecBolusAlarmEditor.swift; sourceTree = "<group>"; };
 		DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EKEventStore+Extensions.swift"; sourceTree = "<group>"; };
 		DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmnipodDashHeartbeatBluetoothTransmitter.swift; sourceTree = "<group>"; };
 		DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseConversion.swift; sourceTree = "<group>"; };
@@ -714,6 +718,7 @@
 		DD02475A2DB2E8CE00FCADF6 /* AlarmCondition */ = {
 			isa = PBXGroup;
 			children = (
+				DD7F4C162DD63FA700D449E9 /* RecBolusCondition.swift */,
 				DD7F4C122DD51FD500D449E9 /* TempTargetEndCondition.swift */,
 				DD7F4C0E2DD51EC200D449E9 /* TempTargetStartCondition.swift */,
 				DD7F4C0A2DD51C5500D449E9 /* OverrideEndCondition.swift */,
@@ -970,6 +975,7 @@
 		DDC7E53F2DBD8A1600EB1127 /* Editors */ = {
 			isa = PBXGroup;
 			children = (
+				DD7F4C182DD63FD500D449E9 /* RecBolusAlarmEditor.swift */,
 				DD7F4C142DD51FEB00D449E9 /* TempTargetEndAlarmEditor.swift */,
 				DD7F4C102DD51ED900D449E9 /* TempTargetStartAlarmEditor.swift */,
 				DD7F4C0C2DD51C8100D449E9 /* OverrideEndAlarmEditor.swift */,
@@ -1729,6 +1735,7 @@
 				DD7F4C072DD5042F00D449E9 /* OverrideStartAlarmEditor.swift in Sources */,
 				DDDF6F492D479AF000884336 /* NoRemoteView.swift in Sources */,
 				DD0650ED2DCE9371004D3B41 /* HighBgAlarmEditor.swift in Sources */,
+				DD7F4C172DD63FA700D449E9 /* RecBolusCondition.swift in Sources */,
 				FCC6886F2489A53800A0279D /* AppConstants.swift in Sources */,
 				DD7E19842ACDA50C00DBD158 /* Overrides.swift in Sources */,
 				DDF2C0102BEFA991007A20E6 /* GitHubService.swift in Sources */,
@@ -1810,6 +1817,7 @@
 				DD58171A2D299EF80041FB98 /* DexcomHeartbeatBluetoothDevice.swift in Sources */,
 				DD7F4C0B2DD51C5500D449E9 /* OverrideEndCondition.swift in Sources */,
 				DDFF3D872D14280500BF9D9E /* BackgroundRefreshSettingsViewModel.swift in Sources */,
+				DD7F4C192DD63FD500D449E9 /* RecBolusAlarmEditor.swift in Sources */,
 				DD50C7502D0828800057AE6F /* ContactSettingsViewModel.swift in Sources */,
 				DDAD162F2D2EF9830084BE10 /* RileyLinkHeartbeatBluetoothDevice.swift in Sources */,
 				FC97881C2485969B00A7906C /* MainViewController.swift in Sources */,

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

@@ -0,0 +1,36 @@
+//
+//  RecBolusCondition.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2025-05-15.
+//  Copyright © 2025 Jon Fawcett. All rights reserved.
+//
+
+import Foundation
+
+/// Fires once when the recommended bolus (units) is ≥ the user-set threshold.
+struct RecBolusCondition: AlarmCondition {
+    static let type: AlarmType = .recBolus
+    init() {}
+
+    func evaluate(alarm: Alarm, data: AlarmData) -> Bool {
+        // ────────────────────────────────
+        // 0. sanity checks
+        // ────────────────────────────────
+        guard let threshold = alarm.threshold, threshold > 0 else { return false }
+        guard let rec = data.latestRecBolus, rec >= threshold else {
+            Storage.shared.lastRecBolusNotified.value = nil
+            return false
+        }
+
+        // ────────────────────────────────
+        // 1. has it INCREASED past the last-notified value?
+        // ────────────────────────────────
+        if let last = Storage.shared.lastRecBolusNotified.value {
+            if rec <= last + 1e-4 { return false }
+        }
+
+        Storage.shared.lastRecBolusNotified.value = rec
+        return true
+    }
+}

+ 1 - 2
LoopFollow/Alarm/AlarmData.swift

@@ -17,6 +17,7 @@ struct AlarmData: Codable {
     let latestOverrideEnd: TimeInterval?
     let latestTempTargetStart: TimeInterval?
     let latestTempTargetEnd: TimeInterval?
+    let latestRecBolus: Double?
 }
 
 /*
@@ -25,7 +26,5 @@ struct AlarmData: Codable {
  //    let latestBoluses: [BolusEntry]
  //    let batteryLevel: Double?
  //    let latestCarbs: [CarbEntry]
- //    let overrideData: [OverrideEntry]
- //    let tempTargetData: [TempTargetEntry]
  //    let pumpVolume: Double?
  */

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

@@ -61,6 +61,8 @@ struct AlarmEditor: View {
             TempTargetStartAlarmEditor(alarm: $alarm)
         case .tempTargetEnd:
             TempTargetEndAlarmEditor(alarm: $alarm)
+        case .recBolus:
+            RecBolusAlarmEditor(alarm: $alarm)
         /* TODO: add other condition types here */
         default:
             Text("No editor for \(alarm.type.rawValue)")

+ 42 - 0
LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift

@@ -0,0 +1,42 @@
+//
+//  RecBolusAlarmEditor.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2025-05-15.
+//  Copyright © 2025 Jon Fawcett. All rights reserved.
+//
+
+import SwiftUI
+
+struct RecBolusAlarmEditor: View {
+    @Binding var alarm: Alarm
+
+    var body: some View {
+        Form {
+            InfoBanner(
+                text: "Alerts when the recommended bolus equals or exceeds the " +
+                    "threshold you set below.",
+                alarmType: alarm.type
+            )
+
+            AlarmGeneralSection(alarm: $alarm)
+
+            AlarmStepperSection(
+                header: "Threshold",
+                footer: "Alert when recommended bolus ≥ this value.",
+                title: "Units",
+                range: 0.1 ... 50,
+                step: 0.1,
+                value: Binding(
+                    get: { alarm.threshold ?? 1.0 },
+                    set: { alarm.threshold = $0 }
+                )
+            )
+
+            AlarmActiveSection(alarm: $alarm)
+            AlarmAudioSection(alarm: $alarm)
+            AlarmSnoozeSection(alarm: $alarm, range: 5 ... 60, step: 5)
+        }
+        .navigationTitle(alarm.type.rawValue)
+    }
+}

+ 1 - 0
LoopFollow/Alarm/AlarmManager.swift

@@ -26,6 +26,7 @@ class AlarmManager {
             OverrideEndCondition.self,
             TempTargetStartCondition.self,
             TempTargetEndCondition.self,
+            RecBolusCondition.self,
             // TODO: add other condition types here
         ]
     ) {

+ 1 - 0
LoopFollow/Storage/Storage.swift

@@ -57,6 +57,7 @@ class Storage {
     var lastOverrideEndNotified = StorageValue<TimeInterval?>(key: "lastOverrideEndNotified", defaultValue: nil)
     var lastTempTargetStartNotified = StorageValue<TimeInterval?>(key: "lastTempTargetStartNotified", defaultValue: nil)
     var lastTempTargetEndNotified = StorageValue<TimeInterval?>(key: "lastTempTargetEndNotified", defaultValue: nil)
+    var lastRecBolusNotified = StorageValue<Double?>(key: "lastRecBolusNotified", defaultValue: nil)
 
     static let shared = Storage()
     private init() {}

+ 2 - 0
LoopFollow/Task/AlarmTask.swift

@@ -23,6 +23,7 @@ extension MainViewController {
             let latestOverrideEnd = self.overrideGraphData.last?.endDate
             let latestTempTargetStart = self.tempTargetGraphData.last?.date
             let latestTempTargetEnd = self.tempTargetGraphData.last?.endDate
+            let latestRecBolus = UserDefaultsRepository.deviceRecBolus.value
 
             let alarmData = AlarmData(
                 bgReadings: self.bgData
@@ -37,6 +38,7 @@ extension MainViewController {
                 latestOverrideEnd: latestOverrideEnd,
                 latestTempTargetStart: latestTempTargetStart,
                 latestTempTargetEnd: latestTempTargetEnd,
+                latestRecBolus: latestRecBolus
             )
 
             let finalAlarmData: AlarmData