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

+ 8 - 0
LoopFollow.xcodeproj/project.pbxproj

@@ -103,6 +103,8 @@
 		DD7F4BC72DD473A600D449E9 /* FastDropCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4BC62DD473A600D449E9 /* FastDropCondition.swift */; };
 		DD7F4C032DD4B92E00D449E9 /* NotLoopingAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C022DD4B92E00D449E9 /* NotLoopingAlarmEditor.swift */; };
 		DD7F4C052DD4BBE200D449E9 /* NotLoopingCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C042DD4BBE200D449E9 /* NotLoopingCondition.swift */; };
+		DD7F4C072DD5042F00D449E9 /* OverrideStartAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C062DD5042F00D449E9 /* OverrideStartAlarmEditor.swift */; };
+		DD7F4C092DD504A700D449E9 /* OverrideStartCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C082DD504A700D449E9 /* OverrideStartCondition.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 */; };
@@ -430,6 +432,8 @@
 		DD7F4BC62DD473A600D449E9 /* FastDropCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastDropCondition.swift; sourceTree = "<group>"; };
 		DD7F4C022DD4B92E00D449E9 /* NotLoopingAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotLoopingAlarmEditor.swift; sourceTree = "<group>"; };
 		DD7F4C042DD4BBE200D449E9 /* NotLoopingCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotLoopingCondition.swift; sourceTree = "<group>"; };
+		DD7F4C062DD5042F00D449E9 /* OverrideStartAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideStartAlarmEditor.swift; sourceTree = "<group>"; };
+		DD7F4C082DD504A700D449E9 /* OverrideStartCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideStartCondition.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>"; };
@@ -699,6 +703,7 @@
 			isa = PBXGroup;
 			children = (
 				DD7F4C042DD4BBE200D449E9 /* NotLoopingCondition.swift */,
+				DD7F4C082DD504A700D449E9 /* OverrideStartCondition.swift */,
 				DD7F4BC62DD473A600D449E9 /* FastDropCondition.swift */,
 				DD0650EA2DCE8385004D3B41 /* LowBGCondition.swift */,
 				DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.swift */,
@@ -950,6 +955,7 @@
 		DDC7E53F2DBD8A1600EB1127 /* Editors */ = {
 			isa = PBXGroup;
 			children = (
+				DD7F4C062DD5042F00D449E9 /* OverrideStartAlarmEditor.swift */,
 				DD7F4C022DD4B92E00D449E9 /* NotLoopingAlarmEditor.swift */,
 				DD0650EC2DCE9371004D3B41 /* HighBgAlarmEditor.swift */,
 				DDC7E53C2DBD8A1600EB1127 /* BuildExpireAlarmEditor.swift */,
@@ -1690,6 +1696,7 @@
 				DDCF9A822D85FD15004DF4DD /* AlarmType.swift in Sources */,
 				DD7FFAFD2A72953000C3A304 /* EKEventStore+Extensions.swift in Sources */,
 				FCC6886724898F8000A0279D /* UserDefaultsValue.swift in Sources */,
+				DD7F4C092DD504A700D449E9 /* OverrideStartCondition.swift in Sources */,
 				DDCF979E24C2382A002C9752 /* AppStateController.swift in Sources */,
 				DDEF503F2D32754F00999A5D /* ProfileTask.swift in Sources */,
 				DD7F4C052DD4BBE200D449E9 /* NotLoopingCondition.swift in Sources */,
@@ -1700,6 +1707,7 @@
 				DDCF979824C1489C002C9752 /* GraphSettingsViewController.swift in Sources */,
 				FC3AE7B5249E8E0E00AAE1E0 /* LoopFollow.xcdatamodeld in Sources */,
 				DDC7E5152DBCFA7900EB1127 /* SnoozerViewController.swift in Sources */,
+				DD7F4C072DD5042F00D449E9 /* OverrideStartAlarmEditor.swift in Sources */,
 				DDDF6F492D479AF000884336 /* NoRemoteView.swift in Sources */,
 				DD0650ED2DCE9371004D3B41 /* HighBgAlarmEditor.swift in Sources */,
 				FCC6886F2489A53800A0279D /* AppConstants.swift in Sources */,

+ 4 - 0
LoopFollow/Alarm/Alarm.swift

@@ -259,12 +259,16 @@ struct Alarm: Identifiable, Codable, Equatable {
             soundFile = .dholShuffleloop
         case .overrideStart:
             soundFile = .endingReached
+            repeatSoundOption = .never
         case .overrideEnd:
             soundFile = .alertToneBusy
+            repeatSoundOption = .never
         case .tempTargetStart:
             soundFile = .endingReached
+            repeatSoundOption = .never
         case .tempTargetEnd:
             soundFile = .alertToneBusy
+            repeatSoundOption = .never
         }
     }
 }

+ 27 - 0
LoopFollow/Alarm/AlarmCondition/OverrideStartCondition.swift

@@ -0,0 +1,27 @@
+//
+//  OverrideStartCondition.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2025-05-14.
+//  Copyright © 2025 Jon Fawcett. All rights reserved.
+//
+
+import Foundation
+
+struct OverrideStartCondition: AlarmCondition {
+    static let type: AlarmType = .overrideStart
+    init() {}
+
+    func evaluate(alarm _: Alarm, data: AlarmData) -> Bool {
+        guard let startTS = data.latestOverrideStart, startTS > 0 else { return false }
+
+        let recent = Date().timeIntervalSince1970 - startTS <= 15 * 60
+        guard recent else { return false }
+
+        let lastNotified = Storage.shared.lastOverrideStartNotified.value ?? 0
+        guard startTS > lastNotified else { return false }
+
+        Storage.shared.lastOverrideStartNotified.value = startTS
+        return true
+    }
+}

+ 1 - 0
LoopFollow/Alarm/AlarmData.swift

@@ -13,6 +13,7 @@ struct AlarmData: Codable {
     let predictionData: [GlucoseValue]
     let expireDate: Date?
     let lastLoopTime: TimeInterval?
+    let latestOverrideStart: TimeInterval?
 }
 
 /*

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

@@ -53,6 +53,8 @@ struct AlarmEditor: View {
             FastDropAlarmEditor(alarm: $alarm)
         case .notLooping:
             NotLoopingAlarmEditor(alarm: $alarm)
+        case .overrideStart:
+            OverrideStartAlarmEditor(alarm: $alarm)
         /* TODO: add other condition types here */
         default:
             Text("No editor for \(alarm.type.rawValue)")

+ 9 - 5
LoopFollow/Alarm/AlarmEditing/Components/AlarmAudioSection.swift

@@ -10,6 +10,7 @@ import SwiftUI
 
 struct AlarmAudioSection: View {
     @Binding var alarm: Alarm
+    var hideRepeat: Bool = false
     @State private var showingTonePicker = false
 
     var body: some View {
@@ -36,11 +37,14 @@ struct AlarmAudioSection: View {
                 selection: $alarm.playSoundOption,
                 allowed: PlaySoundOption.allowed(for: alarm.activeOption)
             )
-            AlarmEnumMenuPicker(
-                title: "Repeat",
-                selection: $alarm.repeatSoundOption,
-                allowed: RepeatSoundOption.allowed(for: alarm.activeOption)
-            )
+
+            if !hideRepeat {
+                AlarmEnumMenuPicker(
+                    title: "Repeat",
+                    selection: $alarm.repeatSoundOption,
+                    allowed: RepeatSoundOption.allowed(for: alarm.activeOption)
+                )
+            }
         }.onChange(of: alarm.activeOption) { newActive in
             let playAllowed = PlaySoundOption.allowed(for: newActive)
             if !playAllowed.contains(alarm.playSoundOption) {

+ 33 - 0
LoopFollow/Alarm/AlarmEditing/Editors/OverrideStartAlarmEditor.swift

@@ -0,0 +1,33 @@
+//
+//  OverrideStartAlarmEditor.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2025-05-14.
+//  Copyright © 2025 Jon Fawcett. All rights reserved.
+//
+
+import SwiftUI
+
+struct OverrideStartAlarmEditor: View {
+    @Binding var alarm: Alarm
+
+    var body: some View {
+        Form {
+            InfoBanner(
+                text: "Alerts when an override begins.",
+                alarmType: alarm.type
+            )
+
+            AlarmGeneralSection(alarm: $alarm)
+
+            AlarmActiveSection(alarm: $alarm)
+            AlarmAudioSection(alarm: $alarm, hideRepeat: true)
+            AlarmSnoozeSection(
+                alarm: $alarm,
+                range: 10 ... 60,
+                step: 5
+            )
+        }
+        .navigationTitle(alarm.type.rawValue)
+    }
+}

+ 1 - 0
LoopFollow/Alarm/AlarmManager.swift

@@ -22,6 +22,7 @@ class AlarmManager {
             HighBGCondition.self,
             FastDropCondition.self,
             NotLoopingCondition.self,
+            OverrideStartCondition.self,
             // TODO: add other condition types here
         ]
     ) {

+ 3 - 4
LoopFollow/Storage/Storage.swift

@@ -51,10 +51,9 @@ class Storage {
     var sensorScheduleOffset = StorageValue<Double?>(key: "sensorScheduleOffset", defaultValue: nil)
 
     var alarms = StorageValue<[Alarm]>(key: "alarms", defaultValue: [])
-    var alarmConfiguration = StorageValue<AlarmConfiguration>(
-        key: "alarmConfiguration",
-        defaultValue: .default
-    )
+    var alarmConfiguration = StorageValue<AlarmConfiguration>(key: "alarmConfiguration", defaultValue: .default)
+
+    var lastOverrideStartNotified = StorageValue<TimeInterval?>(key: "lastOverrideStartNotified", defaultValue: nil)
 
     static let shared = Storage()
     private init() {}

+ 4 - 1
LoopFollow/Task/AlarmTask.swift

@@ -19,6 +19,8 @@ extension MainViewController {
 
     func alarmTaskAction() {
         DispatchQueue.main.async {
+            let latestOverrideStart = self.overrideGraphData.last?.date
+
             let alarmData = AlarmData(
                 bgReadings: self.bgData
                     .suffix(24)
@@ -27,7 +29,8 @@ extension MainViewController {
                     .prefix(12)
                     .map { GlucoseValue(sgv: $0.sgv, date: Date(timeIntervalSince1970: $0.date)) }, /// These are oldest .. newest, Predictions not currently available for Trio
                 expireDate: Storage.shared.expirationDate.value,
-                lastLoopTime: UserDefaultsRepository.alertLastLoopTime.value
+                lastLoopTime: UserDefaultsRepository.alertLastLoopTime.value,
+                latestOverrideStart: latestOverrideStart
             )
 
             let finalAlarmData: AlarmData