diff --git a/LoopFollow/Alarm/Alarm.swift b/LoopFollow/Alarm/Alarm.swift index 2de36fb0f..33e1286d4 100644 --- a/LoopFollow/Alarm/Alarm.swift +++ b/LoopFollow/Alarm/Alarm.swift @@ -76,6 +76,17 @@ struct Alarm: Identifiable, Codable, Equatable { /// Number of minutes that must satisfy the alarm criteria 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, var monitoringWindow: Int? @@ -101,7 +112,8 @@ struct Alarm: Identifiable, Codable, Equatable { enum CodingKeys: String, CodingKey { case id, type, name, isEnabled, snoozedUntil case aboveBG, belowBG, threshold, predictiveMinutes, delta - case persistentMinutes, monitoringWindow, soundFile + case persistentMinutes, suppressIfRising, suppressIfFalling, suppressIfCharging + case monitoringWindow, soundFile case snoozeDuration, playSoundOption, repeatSoundOption case soundDelay, activeOption case missedBolusPrebolusWindow, missedBolusIgnoreSmallBolusUnits @@ -124,6 +136,9 @@ struct Alarm: Identifiable, Codable, Equatable { predictiveMinutes = try container.decodeIfPresent(Int.self, forKey: .predictiveMinutes) delta = try container.decodeIfPresent(Double.self, forKey: .delta) 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) soundFile = try container.decode(SoundFile.self, forKey: .soundFile) 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(delta, forKey: .delta) 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.encode(soundFile, forKey: .soundFile) try container.encode(snoozeDuration, forKey: .snoozeDuration) diff --git a/LoopFollow/Alarm/AlarmCondition/BatteryCondition.swift b/LoopFollow/Alarm/AlarmCondition/BatteryCondition.swift index 1fcd80acf..f5618832e 100644 --- a/LoopFollow/Alarm/AlarmCondition/BatteryCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/BatteryCondition.swift @@ -11,6 +11,11 @@ struct BatteryCondition: AlarmCondition { guard let limit = alarm.threshold, limit > 0 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 } } diff --git a/LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift b/LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift index 26ea46083..6ba8b9b47 100644 --- a/LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift +++ b/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 } } diff --git a/LoopFollow/Alarm/AlarmCondition/LowBGCondition.swift b/LoopFollow/Alarm/AlarmCondition/LowBGCondition.swift index 77abc55a1..0ec27d7bd 100644 --- a/LoopFollow/Alarm/AlarmCondition/LowBGCondition.swift +++ b/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 } diff --git a/LoopFollow/Alarm/AlarmData.swift b/LoopFollow/Alarm/AlarmData.swift index b960a9223..8c1e32503 100644 --- a/LoopFollow/Alarm/AlarmData.swift +++ b/LoopFollow/Alarm/AlarmData.swift @@ -20,6 +20,7 @@ struct AlarmData: Codable { let IOB: Double? let recentBoluses: [BolusEntry] let latestBattery: Double? + let latestBatteryIsCharging: Bool? let latestPumpBattery: Double? let batteryHistory: [DataStructs.batteryStruct] let recentCarbs: [CarbSample] diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift index 38243c22e..1adf217aa 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift @@ -34,6 +34,14 @@ struct HighBgAlarmEditor: View { 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) AlarmAudioSection(alarm: $alarm) AlarmSnoozeSection(alarm: $alarm) diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift index 555db90cd..d31e621a4 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift @@ -43,6 +43,14 @@ struct LowBgAlarmEditor: View { 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) AlarmAudioSection(alarm: $alarm) AlarmSnoozeSection(alarm: $alarm) diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/PhoneBatteryAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/PhoneBatteryAlarmEditor.swift index 67c0bd9c9..d08ee68ed 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/PhoneBatteryAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/PhoneBatteryAlarmEditor.swift @@ -25,6 +25,14 @@ struct PhoneBatteryAlarmEditor: View { 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) AlarmAudioSection(alarm: $alarm) AlarmSnoozeSection(alarm: $alarm) diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift index d2d6920d8..c3034cc1d 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift @@ -133,14 +133,16 @@ extension MainViewController { if let uploader = lastDeviceStatus?["uploader"] as? [String: AnyObject], let upbat = uploader["battery"] as? Double { + let isCharging = uploader["isCharging"] as? Bool let batteryText: String - if let isCharging = uploader["isCharging"] as? Bool, isCharging { + if isCharging == true { batteryText = "⚡️ " + String(format: "%.0f", upbat) + "%" } else { batteryText = String(format: "%.0f", upbat) + "%" } infoManager.updateInfoData(type: .battery, value: batteryText) Observable.shared.deviceBatteryLevel.value = upbat + Observable.shared.deviceBatteryIsCharging.value = isCharging let timestamp = uploader["timestamp"] as? Date ?? Date() let currentBattery = DataStructs.batteryStruct(batteryLevel: upbat, timestamp: timestamp) diff --git a/LoopFollow/Storage/Observable.swift b/LoopFollow/Storage/Observable.swift index 4128918db..b69062dd0 100644 --- a/LoopFollow/Storage/Observable.swift +++ b/LoopFollow/Storage/Observable.swift @@ -41,6 +41,7 @@ class Observable { var previousAlertLastLoopTime = ObservableValue(default: nil) var deviceRecBolus = ObservableValue(default: nil) var deviceBatteryLevel = ObservableValue(default: nil) + var deviceBatteryIsCharging = ObservableValue(default: nil) var pumpBatteryLevel = ObservableValue(default: nil) var enactedOrSuggested = ObservableValue(default: nil) diff --git a/LoopFollow/Task/AlarmTask.swift b/LoopFollow/Task/AlarmTask.swift index 0102d66ed..23ca31cd1 100644 --- a/LoopFollow/Task/AlarmTask.swift +++ b/LoopFollow/Task/AlarmTask.swift @@ -50,6 +50,7 @@ extension MainViewController { IOB: self.latestIOB?.value, recentBoluses: bolusEntries, latestBattery: latestBattery, + latestBatteryIsCharging: Observable.shared.deviceBatteryIsCharging.value, latestPumpBattery: latestPumpBattery, batteryHistory: self.deviceBatteryData, recentCarbs: recentCarbs diff --git a/Tests/AlarmConditions/Helpers.swift b/Tests/AlarmConditions/Helpers.swift index d6b05630c..1fb91ba6c 100644 --- a/Tests/AlarmConditions/Helpers.swift +++ b/Tests/AlarmConditions/Helpers.swift @@ -51,6 +51,7 @@ extension AlarmData { IOB: nil, recentBoluses: [], latestBattery: level, + latestBatteryIsCharging: nil, latestPumpBattery: nil, batteryHistory: [], recentCarbs: [] @@ -75,6 +76,7 @@ extension AlarmData { IOB: nil, recentBoluses: [], latestBattery: nil, + latestBatteryIsCharging: nil, latestPumpBattery: nil, batteryHistory: [], recentCarbs: [] @@ -99,6 +101,7 @@ extension AlarmData { IOB: nil, recentBoluses: [], latestBattery: nil, + latestBatteryIsCharging: nil, latestPumpBattery: nil, batteryHistory: [], recentCarbs: carbs