Jonas Björkert 1 tahun lalu
induk
melakukan
b47134cc6b

+ 8 - 0
LoopFollow.xcodeproj/project.pbxproj

@@ -113,6 +113,8 @@
 		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 */; };
+		DD7F4C1B2DD6501D00D449E9 /* COBCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C1A2DD6501D00D449E9 /* COBCondition.swift */; };
+		DD7F4C1D2DD650D500D449E9 /* COBAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C1C2DD650D500D449E9 /* COBAlarmEditor.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 */; };
@@ -450,6 +452,8 @@
 		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>"; };
+		DD7F4C1A2DD6501D00D449E9 /* COBCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = COBCondition.swift; sourceTree = "<group>"; };
+		DD7F4C1C2DD650D500D449E9 /* COBAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = COBAlarmEditor.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>"; };
@@ -718,6 +722,7 @@
 		DD02475A2DB2E8CE00FCADF6 /* AlarmCondition */ = {
 			isa = PBXGroup;
 			children = (
+				DD7F4C1A2DD6501D00D449E9 /* COBCondition.swift */,
 				DD7F4C162DD63FA700D449E9 /* RecBolusCondition.swift */,
 				DD7F4C122DD51FD500D449E9 /* TempTargetEndCondition.swift */,
 				DD7F4C0E2DD51EC200D449E9 /* TempTargetStartCondition.swift */,
@@ -975,6 +980,7 @@
 		DDC7E53F2DBD8A1600EB1127 /* Editors */ = {
 			isa = PBXGroup;
 			children = (
+				DD7F4C1C2DD650D500D449E9 /* COBAlarmEditor.swift */,
 				DD7F4C182DD63FD500D449E9 /* RecBolusAlarmEditor.swift */,
 				DD7F4C142DD51FEB00D449E9 /* TempTargetEndAlarmEditor.swift */,
 				DD7F4C102DD51ED900D449E9 /* TempTargetStartAlarmEditor.swift */,
@@ -1713,6 +1719,7 @@
 				DD9ACA102D34129200415D8A /* Task.swift in Sources */,
 				DD4AFB492DB576C200BB593F /* AlarmSettingsView.swift in Sources */,
 				DD7F4C032DD4B92E00D449E9 /* NotLoopingAlarmEditor.swift in Sources */,
+				DD7F4C1B2DD6501D00D449E9 /* COBCondition.swift in Sources */,
 				DD4878202C7DAF890048F05C /* PushMessage.swift in Sources */,
 				DD0247712DB4337700FCADF6 /* BuildExpireCondition.swift in Sources */,
 				DDF6999B2C5AA32E0058A8D9 /* TempTargetPreset.swift in Sources */,
@@ -1758,6 +1765,7 @@
 				DDBE3ABD2CB5A961006B37DC /* OverrideView.swift in Sources */,
 				DDB0AF522BB1A8BE00AFA48B /* BuildDetails.swift in Sources */,
 				DDA9ACAA2D6A6B8300E6F1A9 /* ContactIncludeOption.swift in Sources */,
+				DD7F4C1D2DD650D500D449E9 /* COBAlarmEditor.swift in Sources */,
 				DD0C0C622C4175FD00DBADDF /* NSProfile.swift in Sources */,
 				DD58171E2D299FCA0041FB98 /* BluetoothDeviceDelegate.swift in Sources */,
 				DDE69ED22C7256260013EAEC /* RemoteType.swift in Sources */,

+ 31 - 0
LoopFollow/Alarm/AlarmCondition/COBCondition.swift

@@ -0,0 +1,31 @@
+//
+//  COBCondition.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2025-05-15.
+//  Copyright © 2025 Jon Fawcett. All rights reserved.
+//
+
+import Foundation
+
+struct COBCondition: AlarmCondition {
+    static let type: AlarmType = .cob
+    init() {}
+
+    func evaluate(alarm: Alarm, data: AlarmData) -> Bool {
+        guard let threshold = alarm.threshold, threshold > 0 else { return false }
+        guard let cob = data.COB, cob >= threshold else {
+            Storage.shared.lastCOBNotified.value = nil
+            return false
+        }
+
+        if let last = Storage.shared.lastCOBNotified.value,
+           !(cob > last)
+        {
+            return false
+        }
+
+        Storage.shared.lastCOBNotified.value = cob
+        return true
+    }
+}

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

@@ -18,7 +18,7 @@ struct RecBolusCondition: AlarmCondition {
         // 0. sanity checks
         // ────────────────────────────────
         guard let threshold = alarm.threshold, threshold > 0 else { return false }
-        guard let rec = data.latestRecBolus, rec >= threshold else {
+        guard let rec = data.recBolus, rec >= threshold else {
             Storage.shared.lastRecBolusNotified.value = nil
             return false
         }

+ 2 - 2
LoopFollow/Alarm/AlarmData.swift

@@ -17,12 +17,12 @@ struct AlarmData: Codable {
     let latestOverrideEnd: TimeInterval?
     let latestTempTargetStart: TimeInterval?
     let latestTempTargetEnd: TimeInterval?
-    let latestRecBolus: Double?
+    let recBolus: Double?
+    let COB: Double?
 }
 
 /*
  //    let iob: Double?
- //    let cob: Double?
  //    let latestBoluses: [BolusEntry]
  //    let batteryLevel: Double?
  //    let latestCarbs: [CarbEntry]

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

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

+ 47 - 0
LoopFollow/Alarm/AlarmEditing/Editors/COBAlarmEditor.swift

@@ -0,0 +1,47 @@
+//
+//  COBAlarmEditor.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2025-05-15.
+//  Copyright © 2025 Jon Fawcett. All rights reserved.
+//
+
+import SwiftUI
+
+struct COBAlarmEditor: View {
+    @Binding var alarm: Alarm
+
+    var body: some View {
+        Form {
+            InfoBanner(
+                text: "Alerts when Carbs-on-Board exceeds the amount you set below.",
+                alarmType: alarm.type
+            )
+
+            AlarmGeneralSection(alarm: $alarm)
+
+            AlarmStepperSection(
+                header: "Threshold",
+                footer: "Alert when COB ≥ this many grams.",
+                title: "COB",
+                range: 1 ... 200,
+                step: 1,
+                unitLabel: "g",
+                value: Binding(
+                    get: { alarm.threshold ?? 20 },
+                    set: { alarm.threshold = $0 }
+                )
+            )
+
+            AlarmActiveSection(alarm: $alarm)
+            AlarmAudioSection(alarm: $alarm)
+
+            AlarmSnoozeSection(
+                alarm: $alarm,
+                range: 1 ... 6,
+                step: 1
+            )
+        }
+        .navigationTitle(alarm.type.rawValue)
+    }
+}

+ 2 - 0
LoopFollow/Alarm/AlarmManager.swift

@@ -27,6 +27,8 @@ class AlarmManager {
             TempTargetStartCondition.self,
             TempTargetEndCondition.self,
             RecBolusCondition.self,
+            COBCondition.self,
+            MissedReadingCondition.self,
             // TODO: add other condition types here
         ]
     ) {

+ 2 - 2
LoopFollow/Alarm/AlarmType.swift

@@ -62,11 +62,11 @@ extension AlarmType {
             return .day
         case .low, .high, .fastDrop, .fastRise,
              .missedReading, .notLooping, .missedBolus,
-             .iob, .bolus, .cob, .recBolus,
+             .bolus, .recBolus,
              .overrideStart, .overrideEnd, .tempTargetStart,
              .tempTargetEnd:
             return .minute
-        case .battery, .batteryDrop, .sensorChange, .pumpChange,
+        case .battery, .batteryDrop, .sensorChange, .pumpChange, .cob, .iob,
              .pump:
             return .hour
         }

+ 1 - 0
LoopFollow/Storage/Storage.swift

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

+ 4 - 2
LoopFollow/Task/AlarmTask.swift

@@ -23,7 +23,8 @@ 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 recBolus = UserDefaultsRepository.deviceRecBolus.value
+            let COB = self.latestCOB?.value
 
             let alarmData = AlarmData(
                 bgReadings: self.bgData
@@ -38,7 +39,8 @@ extension MainViewController {
                 latestOverrideEnd: latestOverrideEnd,
                 latestTempTargetStart: latestTempTargetStart,
                 latestTempTargetEnd: latestTempTargetEnd,
-                latestRecBolus: latestRecBolus
+                recBolus: recBolus,
+                COB: COB
             )
 
             let finalAlarmData: AlarmData