Quellcode durchsuchen

Add directional/charging skip toggles to Low, High and Battery alarms (#692)

* Add option to skip low BG alarm while rising

The low alarm can now stay silent while BG is climbing, firing only
when the latest reading is flat or still falling. Off by default, so
existing alarms are unaffected.

* Add skip-if-falling for high alarm and skip-while-charging for battery

The high alarm can now stay silent while BG is falling (mirror of the
low alarm's skip-if-rising). The phone-battery alarm can stay silent
while the phone is charging, using the uploader's charging status from
Nightscout. Both are off by default.
Jonas Björkert vor 1 Woche
Ursprung
Commit
d347bfc94e

+ 19 - 1
LoopFollow/Alarm/Alarm.swift

@@ -76,6 +76,17 @@ struct Alarm: Identifiable, Codable, Equatable {
     /// Number of minutes that must satisfy the alarm criteria
     /// Number of minutes that must satisfy the alarm criteria
     var persistentMinutes: Int?
     var persistentMinutes: Int?
 
 
+    /// When set, the low alarm stays silent while BG is rising (positive delta),
+    /// only firing when the latest reading is flat or still falling.
+    var suppressIfRising: Bool = false
+
+    /// When set, the high alarm stays silent while BG is falling (negative delta),
+    /// only firing when the latest reading is flat or still rising.
+    var suppressIfFalling: Bool = false
+
+    /// When set, the phone-battery alarm stays silent while the phone is charging.
+    var suppressIfCharging: Bool = false
+
     /// Size of window to observe values, for example battery drop of x within this number of minutes,
     /// Size of window to observe values, for example battery drop of x within this number of minutes,
     var monitoringWindow: Int?
     var monitoringWindow: Int?
 
 
@@ -101,7 +112,8 @@ struct Alarm: Identifiable, Codable, Equatable {
     enum CodingKeys: String, CodingKey {
     enum CodingKeys: String, CodingKey {
         case id, type, name, isEnabled, snoozedUntil
         case id, type, name, isEnabled, snoozedUntil
         case aboveBG, belowBG, threshold, predictiveMinutes, delta
         case aboveBG, belowBG, threshold, predictiveMinutes, delta
-        case persistentMinutes, monitoringWindow, soundFile
+        case persistentMinutes, suppressIfRising, suppressIfFalling, suppressIfCharging
+        case monitoringWindow, soundFile
         case snoozeDuration, playSoundOption, repeatSoundOption
         case snoozeDuration, playSoundOption, repeatSoundOption
         case soundDelay, activeOption
         case soundDelay, activeOption
         case missedBolusPrebolusWindow, missedBolusIgnoreSmallBolusUnits
         case missedBolusPrebolusWindow, missedBolusIgnoreSmallBolusUnits
@@ -124,6 +136,9 @@ struct Alarm: Identifiable, Codable, Equatable {
         predictiveMinutes = try container.decodeIfPresent(Int.self, forKey: .predictiveMinutes)
         predictiveMinutes = try container.decodeIfPresent(Int.self, forKey: .predictiveMinutes)
         delta = try container.decodeIfPresent(Double.self, forKey: .delta)
         delta = try container.decodeIfPresent(Double.self, forKey: .delta)
         persistentMinutes = try container.decodeIfPresent(Int.self, forKey: .persistentMinutes)
         persistentMinutes = try container.decodeIfPresent(Int.self, forKey: .persistentMinutes)
+        suppressIfRising = try container.decodeIfPresent(Bool.self, forKey: .suppressIfRising) ?? false
+        suppressIfFalling = try container.decodeIfPresent(Bool.self, forKey: .suppressIfFalling) ?? false
+        suppressIfCharging = try container.decodeIfPresent(Bool.self, forKey: .suppressIfCharging) ?? false
         monitoringWindow = try container.decodeIfPresent(Int.self, forKey: .monitoringWindow)
         monitoringWindow = try container.decodeIfPresent(Int.self, forKey: .monitoringWindow)
         soundFile = try container.decode(SoundFile.self, forKey: .soundFile)
         soundFile = try container.decode(SoundFile.self, forKey: .soundFile)
         snoozeDuration = try container.decodeIfPresent(Int.self, forKey: .snoozeDuration) ?? 5
         snoozeDuration = try container.decodeIfPresent(Int.self, forKey: .snoozeDuration) ?? 5
@@ -154,6 +169,9 @@ struct Alarm: Identifiable, Codable, Equatable {
         try container.encodeIfPresent(predictiveMinutes, forKey: .predictiveMinutes)
         try container.encodeIfPresent(predictiveMinutes, forKey: .predictiveMinutes)
         try container.encodeIfPresent(delta, forKey: .delta)
         try container.encodeIfPresent(delta, forKey: .delta)
         try container.encodeIfPresent(persistentMinutes, forKey: .persistentMinutes)
         try container.encodeIfPresent(persistentMinutes, forKey: .persistentMinutes)
+        try container.encode(suppressIfRising, forKey: .suppressIfRising)
+        try container.encode(suppressIfFalling, forKey: .suppressIfFalling)
+        try container.encode(suppressIfCharging, forKey: .suppressIfCharging)
         try container.encodeIfPresent(monitoringWindow, forKey: .monitoringWindow)
         try container.encodeIfPresent(monitoringWindow, forKey: .monitoringWindow)
         try container.encode(soundFile, forKey: .soundFile)
         try container.encode(soundFile, forKey: .soundFile)
         try container.encode(snoozeDuration, forKey: .snoozeDuration)
         try container.encode(snoozeDuration, forKey: .snoozeDuration)

+ 5 - 0
LoopFollow/Alarm/AlarmCondition/BatteryCondition.swift

@@ -11,6 +11,11 @@ struct BatteryCondition: AlarmCondition {
         guard let limit = alarm.threshold, limit > 0 else { return false }
         guard let limit = alarm.threshold, limit > 0 else { return false }
         guard let level = data.latestBattery else { return false }
         guard let level = data.latestBattery else { return false }
 
 
+        // Optional: stay silent while the phone is charging back up.
+        if alarm.suppressIfCharging, data.latestBatteryIsCharging == true {
+            return false
+        }
+
         return level <= limit
         return level <= limit
     }
     }
 }
 }

+ 10 - 0
LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift

@@ -35,6 +35,16 @@ struct HighBGCondition: AlarmCondition {
             }
             }
         }
         }
 
 
+        // Optional: stay silent while the latest reading is below the previous one;
+        // only alarm when BG is flat or still rising.
+        if alarm.suppressIfFalling, data.bgReadings.count >= 2 {
+            let last = data.bgReadings[data.bgReadings.count - 1]
+            let previous = data.bgReadings[data.bgReadings.count - 2]
+            if last.sgv > 0, previous.sgv > 0, last.sgv < previous.sgv {
+                return false
+            }
+        }
+
         return persistentOK
         return persistentOK
     }
     }
 }
 }

+ 14 - 1
LoopFollow/Alarm/AlarmCondition/LowBGCondition.swift

@@ -58,7 +58,20 @@ struct LowBGCondition: AlarmCondition {
         }
         }
 
 
         // ────────────────────────────────
         // ────────────────────────────────
-        // 3. final decision
+        // 3. rising BG suppression (optional)
+        //    Stay silent while the latest reading is above the previous one;
+        //    only alarm when BG is flat or still falling.
+        // ────────────────────────────────
+        if alarm.suppressIfRising, data.bgReadings.count >= 2 {
+            let last = data.bgReadings[data.bgReadings.count - 1]
+            let previous = data.bgReadings[data.bgReadings.count - 2]
+            if last.sgv > 0, previous.sgv > 0, last.sgv > previous.sgv {
+                return false
+            }
+        }
+
+        // ────────────────────────────────
+        // 4. final decision
         // ────────────────────────────────
         // ────────────────────────────────
         return persistentOK || predictiveTrigger
         return persistentOK || predictiveTrigger
     }
     }

+ 1 - 0
LoopFollow/Alarm/AlarmData.swift

@@ -20,6 +20,7 @@ struct AlarmData: Codable {
     let IOB: Double?
     let IOB: Double?
     let recentBoluses: [BolusEntry]
     let recentBoluses: [BolusEntry]
     let latestBattery: Double?
     let latestBattery: Double?
+    let latestBatteryIsCharging: Bool?
     let latestPumpBattery: Double?
     let latestPumpBattery: Double?
     let batteryHistory: [DataStructs.batteryStruct]
     let batteryHistory: [DataStructs.batteryStruct]
     let recentCarbs: [CarbSample]
     let recentCarbs: [CarbSample]

+ 8 - 0
LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift

@@ -34,6 +34,14 @@ struct HighBgAlarmEditor: View {
                 value: $alarm.persistentMinutes
                 value: $alarm.persistentMinutes
             )
             )
 
 
+            Section(
+                header: Text("FALLING BG"),
+                footer: Text("Stay silent while BG is falling. The alert only sounds "
+                    + "when the latest reading is flat or still rising.")
+            ) {
+                Toggle("Skip if BG is falling", isOn: $alarm.suppressIfFalling)
+            }
+
             AlarmActiveSection(alarm: $alarm)
             AlarmActiveSection(alarm: $alarm)
             AlarmAudioSection(alarm: $alarm)
             AlarmAudioSection(alarm: $alarm)
             AlarmSnoozeSection(alarm: $alarm)
             AlarmSnoozeSection(alarm: $alarm)

+ 8 - 0
LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift

@@ -43,6 +43,14 @@ struct LowBgAlarmEditor: View {
                 value: $alarm.predictiveMinutes
                 value: $alarm.predictiveMinutes
             )
             )
 
 
+            Section(
+                header: Text("RISING BG"),
+                footer: Text("Stay silent while BG is rising. The alert only sounds "
+                    + "when the latest reading is flat or still falling.")
+            ) {
+                Toggle("Skip if BG is rising", isOn: $alarm.suppressIfRising)
+            }
+
             AlarmActiveSection(alarm: $alarm)
             AlarmActiveSection(alarm: $alarm)
             AlarmAudioSection(alarm: $alarm)
             AlarmAudioSection(alarm: $alarm)
             AlarmSnoozeSection(alarm: $alarm)
             AlarmSnoozeSection(alarm: $alarm)

+ 8 - 0
LoopFollow/Alarm/AlarmEditing/Editors/PhoneBatteryAlarmEditor.swift

@@ -25,6 +25,14 @@ struct PhoneBatteryAlarmEditor: View {
                 value: $alarm.threshold
                 value: $alarm.threshold
             )
             )
 
 
+            Section(
+                header: Text("CHARGING"),
+                footer: Text("Stay silent while the phone is charging. Requires the "
+                    + "uploader to report charging status; if it doesn't, the alert still sounds.")
+            ) {
+                Toggle("Skip while charging", isOn: $alarm.suppressIfCharging)
+            }
+
             AlarmActiveSection(alarm: $alarm)
             AlarmActiveSection(alarm: $alarm)
             AlarmAudioSection(alarm: $alarm)
             AlarmAudioSection(alarm: $alarm)
             AlarmSnoozeSection(alarm: $alarm)
             AlarmSnoozeSection(alarm: $alarm)

+ 3 - 1
LoopFollow/Controllers/Nightscout/DeviceStatus.swift

@@ -134,14 +134,16 @@ extension MainViewController {
             if let uploader = lastDeviceStatus?["uploader"] as? [String: AnyObject],
             if let uploader = lastDeviceStatus?["uploader"] as? [String: AnyObject],
                let upbat = uploader["battery"] as? Double
                let upbat = uploader["battery"] as? Double
             {
             {
+                let isCharging = uploader["isCharging"] as? Bool
                 let batteryText: String
                 let batteryText: String
-                if let isCharging = uploader["isCharging"] as? Bool, isCharging {
+                if isCharging == true {
                     batteryText = "⚡️ " + String(format: "%.0f", upbat) + "%"
                     batteryText = "⚡️ " + String(format: "%.0f", upbat) + "%"
                 } else {
                 } else {
                     batteryText = String(format: "%.0f", upbat) + "%"
                     batteryText = String(format: "%.0f", upbat) + "%"
                 }
                 }
                 infoManager.updateInfoData(type: .battery, value: batteryText)
                 infoManager.updateInfoData(type: .battery, value: batteryText)
                 Observable.shared.deviceBatteryLevel.value = upbat
                 Observable.shared.deviceBatteryLevel.value = upbat
+                Observable.shared.deviceBatteryIsCharging.value = isCharging
 
 
                 let timestamp = uploader["timestamp"] as? Date ?? Date()
                 let timestamp = uploader["timestamp"] as? Date ?? Date()
                 let currentBattery = DataStructs.batteryStruct(batteryLevel: upbat, timestamp: timestamp)
                 let currentBattery = DataStructs.batteryStruct(batteryLevel: upbat, timestamp: timestamp)

+ 1 - 0
LoopFollow/Storage/Observable.swift

@@ -41,6 +41,7 @@ class Observable {
     var previousAlertLastLoopTime = ObservableValue<TimeInterval?>(default: nil)
     var previousAlertLastLoopTime = ObservableValue<TimeInterval?>(default: nil)
     var deviceRecBolus = ObservableValue<Double?>(default: nil)
     var deviceRecBolus = ObservableValue<Double?>(default: nil)
     var deviceBatteryLevel = ObservableValue<Double?>(default: nil)
     var deviceBatteryLevel = ObservableValue<Double?>(default: nil)
+    var deviceBatteryIsCharging = ObservableValue<Bool?>(default: nil)
     var pumpBatteryLevel = ObservableValue<Double?>(default: nil)
     var pumpBatteryLevel = ObservableValue<Double?>(default: nil)
     var dbSizePercentage = ObservableValue<Double?>(default: nil)
     var dbSizePercentage = ObservableValue<Double?>(default: nil)
     var enactedOrSuggested = ObservableValue<TimeInterval?>(default: nil)
     var enactedOrSuggested = ObservableValue<TimeInterval?>(default: nil)

+ 1 - 0
LoopFollow/Task/AlarmTask.swift

@@ -50,6 +50,7 @@ extension MainViewController {
                 IOB: self.latestIOB?.value,
                 IOB: self.latestIOB?.value,
                 recentBoluses: bolusEntries,
                 recentBoluses: bolusEntries,
                 latestBattery: latestBattery,
                 latestBattery: latestBattery,
+                latestBatteryIsCharging: Observable.shared.deviceBatteryIsCharging.value,
                 latestPumpBattery: latestPumpBattery,
                 latestPumpBattery: latestPumpBattery,
                 batteryHistory: self.deviceBatteryData,
                 batteryHistory: self.deviceBatteryData,
                 recentCarbs: recentCarbs,
                 recentCarbs: recentCarbs,

+ 3 - 0
Tests/AlarmConditions/Helpers.swift

@@ -51,6 +51,7 @@ extension AlarmData {
             IOB: nil,
             IOB: nil,
             recentBoluses: [],
             recentBoluses: [],
             latestBattery: level,
             latestBattery: level,
+            latestBatteryIsCharging: nil,
             latestPumpBattery: nil,
             latestPumpBattery: nil,
             batteryHistory: [],
             batteryHistory: [],
             recentCarbs: []
             recentCarbs: []
@@ -75,6 +76,7 @@ extension AlarmData {
             IOB: nil,
             IOB: nil,
             recentBoluses: [],
             recentBoluses: [],
             latestBattery: nil,
             latestBattery: nil,
+            latestBatteryIsCharging: nil,
             latestPumpBattery: nil,
             latestPumpBattery: nil,
             batteryHistory: [],
             batteryHistory: [],
             recentCarbs: []
             recentCarbs: []
@@ -99,6 +101,7 @@ extension AlarmData {
             IOB: nil,
             IOB: nil,
             recentBoluses: [],
             recentBoluses: [],
             latestBattery: nil,
             latestBattery: nil,
+            latestBatteryIsCharging: nil,
             latestPumpBattery: nil,
             latestPumpBattery: nil,
             batteryHistory: [],
             batteryHistory: [],
             recentCarbs: carbs
             recentCarbs: carbs