Skip to content

Commit d347bfc

Browse files
authored
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.
1 parent 01e0786 commit d347bfc

12 files changed

Lines changed: 81 additions & 3 deletions

File tree

LoopFollow/Alarm/Alarm.swift

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,17 @@ struct Alarm: Identifiable, Codable, Equatable {
7676
/// Number of minutes that must satisfy the alarm criteria
7777
var persistentMinutes: Int?
7878

79+
/// When set, the low alarm stays silent while BG is rising (positive delta),
80+
/// only firing when the latest reading is flat or still falling.
81+
var suppressIfRising: Bool = false
82+
83+
/// When set, the high alarm stays silent while BG is falling (negative delta),
84+
/// only firing when the latest reading is flat or still rising.
85+
var suppressIfFalling: Bool = false
86+
87+
/// When set, the phone-battery alarm stays silent while the phone is charging.
88+
var suppressIfCharging: Bool = false
89+
7990
/// Size of window to observe values, for example battery drop of x within this number of minutes,
8091
var monitoringWindow: Int?
8192

@@ -101,7 +112,8 @@ struct Alarm: Identifiable, Codable, Equatable {
101112
enum CodingKeys: String, CodingKey {
102113
case id, type, name, isEnabled, snoozedUntil
103114
case aboveBG, belowBG, threshold, predictiveMinutes, delta
104-
case persistentMinutes, monitoringWindow, soundFile
115+
case persistentMinutes, suppressIfRising, suppressIfFalling, suppressIfCharging
116+
case monitoringWindow, soundFile
105117
case snoozeDuration, playSoundOption, repeatSoundOption
106118
case soundDelay, activeOption
107119
case missedBolusPrebolusWindow, missedBolusIgnoreSmallBolusUnits
@@ -124,6 +136,9 @@ struct Alarm: Identifiable, Codable, Equatable {
124136
predictiveMinutes = try container.decodeIfPresent(Int.self, forKey: .predictiveMinutes)
125137
delta = try container.decodeIfPresent(Double.self, forKey: .delta)
126138
persistentMinutes = try container.decodeIfPresent(Int.self, forKey: .persistentMinutes)
139+
suppressIfRising = try container.decodeIfPresent(Bool.self, forKey: .suppressIfRising) ?? false
140+
suppressIfFalling = try container.decodeIfPresent(Bool.self, forKey: .suppressIfFalling) ?? false
141+
suppressIfCharging = try container.decodeIfPresent(Bool.self, forKey: .suppressIfCharging) ?? false
127142
monitoringWindow = try container.decodeIfPresent(Int.self, forKey: .monitoringWindow)
128143
soundFile = try container.decode(SoundFile.self, forKey: .soundFile)
129144
snoozeDuration = try container.decodeIfPresent(Int.self, forKey: .snoozeDuration) ?? 5
@@ -154,6 +169,9 @@ struct Alarm: Identifiable, Codable, Equatable {
154169
try container.encodeIfPresent(predictiveMinutes, forKey: .predictiveMinutes)
155170
try container.encodeIfPresent(delta, forKey: .delta)
156171
try container.encodeIfPresent(persistentMinutes, forKey: .persistentMinutes)
172+
try container.encode(suppressIfRising, forKey: .suppressIfRising)
173+
try container.encode(suppressIfFalling, forKey: .suppressIfFalling)
174+
try container.encode(suppressIfCharging, forKey: .suppressIfCharging)
157175
try container.encodeIfPresent(monitoringWindow, forKey: .monitoringWindow)
158176
try container.encode(soundFile, forKey: .soundFile)
159177
try container.encode(snoozeDuration, forKey: .snoozeDuration)

LoopFollow/Alarm/AlarmCondition/BatteryCondition.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ struct BatteryCondition: AlarmCondition {
1111
guard let limit = alarm.threshold, limit > 0 else { return false }
1212
guard let level = data.latestBattery else { return false }
1313

14+
// Optional: stay silent while the phone is charging back up.
15+
if alarm.suppressIfCharging, data.latestBatteryIsCharging == true {
16+
return false
17+
}
18+
1419
return level <= limit
1520
}
1621
}

LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ struct HighBGCondition: AlarmCondition {
3535
}
3636
}
3737

38+
// Optional: stay silent while the latest reading is below the previous one;
39+
// only alarm when BG is flat or still rising.
40+
if alarm.suppressIfFalling, data.bgReadings.count >= 2 {
41+
let last = data.bgReadings[data.bgReadings.count - 1]
42+
let previous = data.bgReadings[data.bgReadings.count - 2]
43+
if last.sgv > 0, previous.sgv > 0, last.sgv < previous.sgv {
44+
return false
45+
}
46+
}
47+
3848
return persistentOK
3949
}
4050
}

LoopFollow/Alarm/AlarmCondition/LowBGCondition.swift

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,20 @@ struct LowBGCondition: AlarmCondition {
5858
}
5959

6060
// ────────────────────────────────
61-
// 3. final decision
61+
// 3. rising BG suppression (optional)
62+
// Stay silent while the latest reading is above the previous one;
63+
// only alarm when BG is flat or still falling.
64+
// ────────────────────────────────
65+
if alarm.suppressIfRising, data.bgReadings.count >= 2 {
66+
let last = data.bgReadings[data.bgReadings.count - 1]
67+
let previous = data.bgReadings[data.bgReadings.count - 2]
68+
if last.sgv > 0, previous.sgv > 0, last.sgv > previous.sgv {
69+
return false
70+
}
71+
}
72+
73+
// ────────────────────────────────
74+
// 4. final decision
6275
// ────────────────────────────────
6376
return persistentOK || predictiveTrigger
6477
}

LoopFollow/Alarm/AlarmData.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ struct AlarmData: Codable {
2020
let IOB: Double?
2121
let recentBoluses: [BolusEntry]
2222
let latestBattery: Double?
23+
let latestBatteryIsCharging: Bool?
2324
let latestPumpBattery: Double?
2425
let batteryHistory: [DataStructs.batteryStruct]
2526
let recentCarbs: [CarbSample]

LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ struct HighBgAlarmEditor: View {
3434
value: $alarm.persistentMinutes
3535
)
3636

37+
Section(
38+
header: Text("FALLING BG"),
39+
footer: Text("Stay silent while BG is falling. The alert only sounds "
40+
+ "when the latest reading is flat or still rising.")
41+
) {
42+
Toggle("Skip if BG is falling", isOn: $alarm.suppressIfFalling)
43+
}
44+
3745
AlarmActiveSection(alarm: $alarm)
3846
AlarmAudioSection(alarm: $alarm)
3947
AlarmSnoozeSection(alarm: $alarm)

LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ struct LowBgAlarmEditor: View {
4343
value: $alarm.predictiveMinutes
4444
)
4545

46+
Section(
47+
header: Text("RISING BG"),
48+
footer: Text("Stay silent while BG is rising. The alert only sounds "
49+
+ "when the latest reading is flat or still falling.")
50+
) {
51+
Toggle("Skip if BG is rising", isOn: $alarm.suppressIfRising)
52+
}
53+
4654
AlarmActiveSection(alarm: $alarm)
4755
AlarmAudioSection(alarm: $alarm)
4856
AlarmSnoozeSection(alarm: $alarm)

LoopFollow/Alarm/AlarmEditing/Editors/PhoneBatteryAlarmEditor.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ struct PhoneBatteryAlarmEditor: View {
2525
value: $alarm.threshold
2626
)
2727

28+
Section(
29+
header: Text("CHARGING"),
30+
footer: Text("Stay silent while the phone is charging. Requires the "
31+
+ "uploader to report charging status; if it doesn't, the alert still sounds.")
32+
) {
33+
Toggle("Skip while charging", isOn: $alarm.suppressIfCharging)
34+
}
35+
2836
AlarmActiveSection(alarm: $alarm)
2937
AlarmAudioSection(alarm: $alarm)
3038
AlarmSnoozeSection(alarm: $alarm)

LoopFollow/Controllers/Nightscout/DeviceStatus.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,14 +134,16 @@ extension MainViewController {
134134
if let uploader = lastDeviceStatus?["uploader"] as? [String: AnyObject],
135135
let upbat = uploader["battery"] as? Double
136136
{
137+
let isCharging = uploader["isCharging"] as? Bool
137138
let batteryText: String
138-
if let isCharging = uploader["isCharging"] as? Bool, isCharging {
139+
if isCharging == true {
139140
batteryText = "⚡️ " + String(format: "%.0f", upbat) + "%"
140141
} else {
141142
batteryText = String(format: "%.0f", upbat) + "%"
142143
}
143144
infoManager.updateInfoData(type: .battery, value: batteryText)
144145
Observable.shared.deviceBatteryLevel.value = upbat
146+
Observable.shared.deviceBatteryIsCharging.value = isCharging
145147

146148
let timestamp = uploader["timestamp"] as? Date ?? Date()
147149
let currentBattery = DataStructs.batteryStruct(batteryLevel: upbat, timestamp: timestamp)

LoopFollow/Storage/Observable.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class Observable {
4141
var previousAlertLastLoopTime = ObservableValue<TimeInterval?>(default: nil)
4242
var deviceRecBolus = ObservableValue<Double?>(default: nil)
4343
var deviceBatteryLevel = ObservableValue<Double?>(default: nil)
44+
var deviceBatteryIsCharging = ObservableValue<Bool?>(default: nil)
4445
var pumpBatteryLevel = ObservableValue<Double?>(default: nil)
4546
var dbSizePercentage = ObservableValue<Double?>(default: nil)
4647
var enactedOrSuggested = ObservableValue<TimeInterval?>(default: nil)

0 commit comments

Comments
 (0)