Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions LoopFollow/Alarm/Alarm.swift
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,8 @@ struct Alarm: Identifiable, Codable, Equatable {

/// Function for when the alarm is triggered.
/// If this alarm, all alarms is disabled or snoozed, then should not be called. This or all alarmd could be muted, then this function will just generate a notification.
func trigger(config: AlarmConfiguration, now: Date) {
LogManager.shared.log(category: .alarm, message: "Alarm triggered: \(type.rawValue)")
func trigger(config: AlarmConfiguration, now: Date, titleOverride: String? = nil) {
LogManager.shared.log(category: .alarm, message: "Alarm triggered: \(titleOverride ?? type.rawValue)")

var playSound = true

Expand Down Expand Up @@ -277,7 +277,7 @@ struct Alarm: Identifiable, Codable, Equatable {
}
}()

AlarmManager.shared.sendNotification(title: type.rawValue, actionTitle: snoozeDuration == 0 ? "Acknowledge" : "Snooze")
AlarmManager.shared.sendNotification(title: titleOverride ?? type.rawValue, actionTitle: snoozeDuration == 0 ? "Acknowledge" : "Snooze")

if playSound {
AlarmSound.setSoundFile(soundFile)
Expand Down
5 changes: 5 additions & 0 deletions LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ protocol AlarmCondition {
init()
/// pure, per-alarm logic against `AlarmData`
func evaluate(alarm: Alarm, data: AlarmData, now: Date) -> Bool
/// Notification title for the firing that just evaluated true; nil (the
/// default) uses the alarm type's name.
func notificationTitle(alarm: Alarm, data: AlarmData, now: Date) -> String?
/// `true` when `belowBG`/`aboveBG` are this condition's own trigger
/// threshold, checked in `evaluate`; `false` (the default) treats them
/// as activation limits enforced by `passesBGLimits`.
Expand All @@ -17,6 +20,8 @@ protocol AlarmCondition {
extension AlarmCondition {
var checksOwnBGLimits: Bool { false }

func notificationTitle(alarm _: Alarm, data _: AlarmData, now _: Date) -> String? { nil }

/// Returns `true` when the alarm is allowed to continue evaluating
/// after BG-limit checks; `false` blocks it immediately.
func passesBGLimits(alarm: Alarm, data: AlarmData) -> Bool {
Expand Down
40 changes: 33 additions & 7 deletions LoopFollow/Alarm/AlarmCondition/OverrideEndCondition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,44 @@

import Foundation

/// Fires once when the active override ends and, if the alarm's
/// `predictiveMinutes` is set, once that many minutes before the scheduled end.
struct OverrideEndCondition: AlarmCondition {
static let type: AlarmType = .overrideEnd
init() {}

func evaluate(alarm _: Alarm, data: AlarmData, now: Date) -> Bool {
guard let endTS = data.latestOverrideEnd, endTS > 0 else { return false }
guard now.timeIntervalSince1970 - endTS <= 15 * 60 else { return false }
func evaluate(alarm: Alarm, data: AlarmData, now: Date) -> Bool {
// The ended phase must stay ahead of the early-warning phase;
// notificationTitle(alarm:data:now:) relies on this ordering.
if let endTS = data.latestOverrideEnd, endTS > 0,
now.timeIntervalSince1970 - endTS <= 15 * 60
{
let last = Storage.shared.lastOverrideEndNotified.value ?? 0
if endTS > last {
Storage.shared.lastOverrideEndNotified.value = endTS
return true
}
}

let last = Storage.shared.lastOverrideEndNotified.value ?? 0
guard endTS > last else { return false }
if let lead = alarm.predictiveMinutes, lead > 0,
let endTS = data.activeOverrideEnd,
now.timeIntervalSince1970 >= endTS - Double(lead) * 60
{
let last = Storage.shared.lastOverridePreEndNotified.value ?? 0
if endTS > last {
Storage.shared.lastOverridePreEndNotified.value = endTS
return true
}
}

Storage.shared.lastOverrideEndNotified.value = endTS
return true
return false
}

func notificationTitle(alarm _: Alarm, data: AlarmData, now: Date) -> String? {
guard let endTS = data.activeOverrideEnd,
now.timeIntervalSince1970 < endTS,
Storage.shared.lastOverridePreEndNotified.value == endTS
else { return nil }
return "Override Ending Soon"
}
}
41 changes: 33 additions & 8 deletions LoopFollow/Alarm/AlarmCondition/TempTargetEndCondition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,44 @@

import Foundation

/// Fires once when the active temp target ends.
/// Fires once when the active temp target ends and, if the alarm's
/// `predictiveMinutes` is set, once that many minutes before the scheduled end.
struct TempTargetEndCondition: AlarmCondition {
static let type: AlarmType = .tempTargetEnd
init() {}

func evaluate(alarm _: Alarm, data: AlarmData, now: Date) -> Bool {
guard let endTS = data.latestTempTargetEnd, endTS > 0 else { return false }
guard now.timeIntervalSince1970 - endTS <= 15 * 60 else { return false }
func evaluate(alarm: Alarm, data: AlarmData, now: Date) -> Bool {
// The ended phase must stay ahead of the early-warning phase;
// notificationTitle(alarm:data:now:) relies on this ordering.
if let endTS = data.latestTempTargetEnd, endTS > 0,
now.timeIntervalSince1970 - endTS <= 15 * 60
{
let last = Storage.shared.lastTempTargetEndNotified.value ?? 0
if endTS > last {
Storage.shared.lastTempTargetEndNotified.value = endTS
return true
}
}

let last = Storage.shared.lastTempTargetEndNotified.value ?? 0
guard endTS > last else { return false }
if let lead = alarm.predictiveMinutes, lead > 0,
let endTS = data.activeTempTargetEnd,
now.timeIntervalSince1970 >= endTS - Double(lead) * 60
{
let last = Storage.shared.lastTempTargetPreEndNotified.value ?? 0
if endTS > last {
Storage.shared.lastTempTargetPreEndNotified.value = endTS
return true
}
}

Storage.shared.lastTempTargetEndNotified.value = endTS
return true
return false
}

func notificationTitle(alarm _: Alarm, data: AlarmData, now: Date) -> String? {
guard let endTS = data.activeTempTargetEnd,
now.timeIntervalSince1970 < endTS,
Storage.shared.lastTempTargetPreEndNotified.value == endTS
else { return nil }
return "Temp Target Ending Soon"
}
}
4 changes: 4 additions & 0 deletions LoopFollow/Alarm/AlarmData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ struct AlarmData: Codable {
let latestOverrideEnd: TimeInterval?
let latestTempTargetStart: TimeInterval?
let latestTempTargetEnd: TimeInterval?
/// Scheduled end of the currently-active override/temp target, if known
/// in advance — used by the end alarms' early warning.
var activeOverrideEnd: TimeInterval?
var activeTempTargetEnd: TimeInterval?
let recBolus: Double?
let COB: Double?
let sageInsertTime: TimeInterval?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,21 @@ struct OverrideEndAlarmEditor: View {

var body: some View {
Group {
InfoBanner(text: "Alerts when an override ends.", alarmType: alarm.type)
InfoBanner(text: "Alerts when an override ends, with an optional early warning before the scheduled end.", alarmType: alarm.type)

AlarmGeneralSection(alarm: $alarm)

AlarmStepperSection(
header: "Early Warning",
footer: "Also alert this many minutes before the override "
+ "is scheduled to end. Set to 0 to alert only when it ends.",
title: "Warn before end",
range: 0 ... 30,
step: 5,
unitLabel: alarm.type.snoozeTimeUnit.label,
value: $alarm.predictiveMinutes
)

AlarmActiveSection(alarm: $alarm)
AlarmAudioSection(alarm: $alarm, hideRepeat: true)
AlarmSnoozeSection(alarm: $alarm)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,21 @@ struct TempTargetEndAlarmEditor: View {

var body: some View {
Group {
InfoBanner(text: "Alerts when a temp target ends.", alarmType: alarm.type)
InfoBanner(text: "Alerts when a temp target ends, with an optional early warning before the scheduled end.", alarmType: alarm.type)

AlarmGeneralSection(alarm: $alarm)

AlarmStepperSection(
header: "Early Warning",
footer: "Also alert this many minutes before the temp target "
+ "is scheduled to end. Set to 0 to alert only when it ends.",
title: "Warn before end",
range: 0 ... 30,
step: 5,
unitLabel: alarm.type.snoozeTimeUnit.label,
value: $alarm.predictiveMinutes
)

AlarmActiveSection(alarm: $alarm)
AlarmAudioSection(alarm: $alarm, hideRepeat: true)
AlarmSnoozeSection(alarm: $alarm)
Expand Down
5 changes: 4 additions & 1 deletion LoopFollow/Alarm/AlarmManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,11 @@ class AlarmManager {
}

// Fire the alarm and break the loop; we only allow one alarm per evaluation tick.
let titleOverride = checker.notificationTitle(alarm: alarm, data: data, now: now)
Observable.shared.currentAlarmTitleOverride.value = titleOverride
Observable.shared.currentAlarm.value = alarm.id

alarm.trigger(config: Storage.shared.alarmConfiguration.value, now: now)
alarm.trigger(config: Storage.shared.alarmConfiguration.value, now: now, titleOverride: titleOverride)

// Store the latest bg time so we don't use it again
if alarm.type.isBGBased,
Expand Down Expand Up @@ -183,6 +185,7 @@ class AlarmManager {
func stopAlarm() {
AlarmSound.stop()
Observable.shared.currentAlarm.value = nil
Observable.shared.currentAlarmTitleOverride.value = nil
UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,9 @@ extension MainViewController {

var end: TimeInterval = isIndefinite ? maxEndDate : start + durationSeconds

// True end for countdown display: based on the raw start and never
// clamped to the graph edge; nil while indefinite.
// True end for countdown display and the end alarm's early
// warning: based on the raw start and never clamped to the graph
// edge; nil while indefinite.
var trueEnd: TimeInterval? = isIndefinite ? nil : startDate.timeIntervalSince1970 + durationSeconds

if let nextStart = nextStart {
Expand Down Expand Up @@ -84,7 +85,8 @@ extension MainViewController {
reason: (e["notes"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
?? (e["reason"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
?? "",
sgv: -20
sgv: -20,
scheduledEndDate: trueEnd
)
overrideGraphData.append(dot)

Expand Down
4 changes: 4 additions & 0 deletions LoopFollow/Helpers/DataStructs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ class DataStructs {
var enteredBy: String
var reason: String
var sgv: Float
/// Scheduled end (start + duration); unlike `endDate` it is never
/// clamped to the graph horizon. Nil when the override runs
/// indefinitely, so the end is not known in advance.
var scheduledEndDate: TimeInterval?
}

struct tempTargetStruct: Codable {
Expand Down
2 changes: 1 addition & 1 deletion LoopFollow/Snoozer/SnoozerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ struct SnoozerView: View {
if let alarm = vm.activeAlarm {
VStack(spacing: 16) {
// Alarm name at the top
Text(alarm.name)
Text(vm.alarmTitle)
.font(.system(size: 30, weight: .semibold))
.foregroundColor(.white)
.lineLimit(1)
Expand Down
2 changes: 2 additions & 0 deletions LoopFollow/Snoozer/SnoozerViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Foundation

final class SnoozerViewModel: ObservableObject {
@Published var activeAlarm: Alarm?
@Published var alarmTitle: String = ""
@Published var snoozeUnits: Int = 5
@Published var timeUnitLabel: String = "minutes"

Expand All @@ -21,6 +22,7 @@ final class SnoozerViewModel: ObservableObject {
.sink { [weak self] alarm in
self?.activeAlarm = alarm
if let a = alarm {
self?.alarmTitle = Observable.shared.currentAlarmTitleOverride.value ?? a.name
self?.snoozeUnits = a.snoozeDuration
self?.timeUnitLabel = a.type.snoozeTimeUnit.label
}
Expand Down
3 changes: 3 additions & 0 deletions LoopFollow/Storage/Observable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ class Observable {
var predictionColor = ObservableValue<Color>(default: .purple)

var currentAlarm = ObservableValue<UUID?>(default: nil)
/// Replaces the current alarm's name on screen when the firing needs its
/// own wording (e.g. an end alarm's early warning); nil shows the name.
var currentAlarmTitleOverride = ObservableValue<String?>(default: nil)
var alarmSoundPlaying = ObservableValue<Bool>(default: false)

var debug = ObservableValue<Bool>(default: false)
Expand Down
2 changes: 2 additions & 0 deletions LoopFollow/Storage/Storage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,10 @@ class Storage {

var lastOverrideStartNotified = StorageValue<TimeInterval?>(key: "lastOverrideStartNotified", defaultValue: nil)
var lastOverrideEndNotified = StorageValue<TimeInterval?>(key: "lastOverrideEndNotified", defaultValue: nil)
var lastOverridePreEndNotified = StorageValue<TimeInterval?>(key: "lastOverridePreEndNotified", defaultValue: nil)
var lastTempTargetStartNotified = StorageValue<TimeInterval?>(key: "lastTempTargetStartNotified", defaultValue: nil)
var lastTempTargetEndNotified = StorageValue<TimeInterval?>(key: "lastTempTargetEndNotified", defaultValue: nil)
var lastTempTargetPreEndNotified = StorageValue<TimeInterval?>(key: "lastTempTargetPreEndNotified", defaultValue: nil)
var lastRecBolusNotified = StorageValue<Double?>(key: "lastRecBolusNotified", defaultValue: nil)
var lastCOBNotified = StorageValue<Double?>(key: "lastCOBNotified", defaultValue: nil)
var lastMissedBolusNotified = StorageValue<Date?>(key: "lastMissedBolusNotified", defaultValue: nil)
Expand Down
4 changes: 4 additions & 0 deletions LoopFollow/Task/AlarmTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ extension MainViewController {
let latestOverrideEnd = self.overrideGraphData.last { $0.endDate <= now }?.endDate
let latestTempTargetStart = self.tempTargetGraphData.last { $0.date <= now }?.date
let latestTempTargetEnd = self.tempTargetGraphData.last { $0.endDate <= now }?.endDate
let activeOverrideEnd = self.overrideGraphData.last { $0.date <= now && ($0.scheduledEndDate ?? 0) > now }?.scheduledEndDate
let activeTempTargetEnd = self.tempTargetGraphData.last { $0.date <= now && $0.endDate > now }?.endDate
let recBolus = Observable.shared.deviceRecBolus.value
let COB = self.latestCOB?.value
let sensorInsertedAt = Storage.shared.sageInsertTime.value
Expand All @@ -40,6 +42,8 @@ extension MainViewController {
latestOverrideEnd: latestOverrideEnd,
latestTempTargetStart: latestTempTargetStart,
latestTempTargetEnd: latestTempTargetEnd,
activeOverrideEnd: activeOverrideEnd,
activeTempTargetEnd: activeTempTargetEnd,
recBolus: recBolus,
COB: COB,
sageInsertTime: sensorInsertedAt,
Expand Down
Loading
Loading