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
4 changes: 4 additions & 0 deletions LoopFollow/Controllers/NightScout.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,15 @@ extension MainViewController {

func clearOldOverride() {
overrideGraphData.removeAll()
Observable.shared.override.value = nil
Observable.shared.overrideEndAt.value = nil
updateOverrideGraph()
}

func clearOldTempTarget() {
tempTargetGraphData.removeAll()
Observable.shared.tempTarget.value = nil
Observable.shared.tempTargetEndAt.value = nil
updateTempTargetGraph()
}

Expand Down
38 changes: 27 additions & 11 deletions LoopFollow/Controllers/Nightscout/Treatments/Overrides.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ extension MainViewController {
func processNSOverrides(entries: [[String: AnyObject]]) {
overrideGraphData.removeAll()
var activeOverrideNote: String?
var activeOverrideEndAt: TimeInterval?

let sorted = entries.sorted { lhs, rhs in
guard
Expand Down Expand Up @@ -35,19 +36,28 @@ extension MainViewController {

let start = max(startDate.timeIntervalSince1970, graphHorizon)

var end: TimeInterval
if (e["durationType"] as? String) == "indefinite" { // Only for Loop overrides
end = maxEndDate
} else {
end = start + (e["duration"] as? Double ?? 5) * 60
}
let nextStart: TimeInterval? = {
guard i + 1 < sorted.count,
let nextDateStr = (sorted[i + 1]["timestamp"] as? String) ?? (sorted[i + 1]["created_at"] as? String)
else { return nil }
return NightscoutUtils.parseDate(nextDateStr)?.timeIntervalSince1970
}()

let durationSeconds = (e["duration"] as? Double ?? 5) * 60
// Loop marks indefinite overrides explicitly; Trio represents them
// as a ~30-day duration. Treat a week or longer as indefinite.
let isIndefinite = (e["durationType"] as? String) == "indefinite"
|| durationSeconds >= 7 * 24 * 3600

if i + 1 < sorted.count,
let nextDateStr = (sorted[i + 1]["timestamp"] as? String) ?? (sorted[i + 1]["created_at"] as? String),
let nextStart = NightscoutUtils.parseDate(nextDateStr)?
.timeIntervalSince1970
{
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.
var trueEnd: TimeInterval? = isIndefinite ? nil : startDate.timeIntervalSince1970 + durationSeconds

if let nextStart = nextStart {
end = min(end, nextStart - 60) // avoid overlapping overrides
trueEnd = trueEnd.map { min($0, nextStart - 60) }
}

end = min(end, maxEndDate)
Expand Down Expand Up @@ -80,10 +90,12 @@ extension MainViewController {

if now >= start, now < end {
activeOverrideNote = e["notes"] as? String ?? e["reason"] as? String
activeOverrideEndAt = trueEnd
}
}

Observable.shared.override.value = activeOverrideNote
Observable.shared.overrideEndAt.value = activeOverrideEndAt
if Storage.shared.device.value != "Loop" {
if let note = activeOverrideNote {
infoManager.updateInfoData(type: .override, value: note)
Expand All @@ -94,5 +106,9 @@ extension MainViewController {
if Storage.shared.graphOtherTreatments.value {
updateOverrideGraph()
}

#if !targetEnvironment(macCatalyst)
LiveActivityManager.shared.refreshFromCurrentState(reason: "overrideChanged")
#endif
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ extension MainViewController {
func processNSTemporaryTarget(entries: [[String: AnyObject]]) {
tempTargetGraphData.removeAll()
var activeTempTarget: Int?
var activeTempTargetEndAt: TimeInterval?

for (index, currentEntry) in entries.reversed().enumerated() {
guard let dateStr = currentEntry["timestamp"] as? String ?? currentEntry["created_at"] as? String else { continue }
Expand All @@ -27,6 +28,7 @@ extension MainViewController {
if let activeIndex = tempTargetGraphData.lastIndex(where: { $0.endDate > dateTimeStamp }) {
tempTargetGraphData[activeIndex].endDate = dateTimeStamp
activeTempTarget = nil
activeTempTargetEndAt = nil
}
continue
}
Expand Down Expand Up @@ -58,6 +60,9 @@ extension MainViewController {
let currentTime = Date().timeIntervalSince1970
if currentTime < endDate {
activeTempTarget = Int(targetValue!)
// Trio represents indefinite temp targets as a ~30-day duration;
// treat a week or longer as indefinite (no end to display).
activeTempTargetEndAt = duration >= 7 * 24 * 3600 ? nil : endDate
}
}

Expand All @@ -72,5 +77,10 @@ extension MainViewController {
} else {
Observable.shared.tempTarget.value = nil
}
Observable.shared.tempTargetEndAt.value = activeTempTargetEndAt

#if !targetEnvironment(macCatalyst)
LiveActivityManager.shared.refreshFromCurrentState(reason: "tempTargetChanged")
#endif
}
}
3 changes: 3 additions & 0 deletions LoopFollow/LiveActivity/APNSClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,9 @@ class APNSClient {
if let cob = snapshot.cob { snapshotDict["cob"] = cob }
if let projected = snapshot.projected { snapshotDict["projected"] = projected }
if let override = snapshot.override { snapshotDict["override"] = override }
if let overrideEndAt = snapshot.overrideEndAt { snapshotDict["overrideEndAt"] = overrideEndAt }
if let tempTargetMgdl = snapshot.tempTargetMgdl { snapshotDict["tempTargetMgdl"] = tempTargetMgdl }
if let tempTargetEndAt = snapshot.tempTargetEndAt { snapshotDict["tempTargetEndAt"] = tempTargetEndAt }
if let recBolus = snapshot.recBolus { snapshotDict["recBolus"] = recBolus }
if let battery = snapshot.battery { snapshotDict["battery"] = battery }
if let pumpBattery = snapshot.pumpBattery { snapshotDict["pumpBattery"] = pumpBattery }
Expand Down
12 changes: 12 additions & 0 deletions LoopFollow/LiveActivity/GlucoseSnapshotBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ protocol CurrentGlucoseStateProviding {
/// Active override name (nil if no active override).
var override: String? { get }

/// End of the active override as Unix epoch seconds (nil = no override or indefinite).
var overrideEndAt: TimeInterval? { get }

/// Active temporary target in mg/dL (nil if no active temp target).
var tempTargetMgdl: Double? { get }

/// End of the active temporary target as Unix epoch seconds.
var tempTargetEndAt: TimeInterval? { get }

/// Recommended bolus in units.
var recBolus: Double? { get }

Expand Down Expand Up @@ -141,6 +150,9 @@ enum GlucoseSnapshotBuilder {
cob: provider.cob,
projected: provider.projectedMgdl,
override: provider.override,
overrideEndAt: provider.overrideEndAt,
tempTargetMgdl: provider.tempTargetMgdl,
tempTargetEndAt: provider.tempTargetEndAt,
recBolus: provider.recBolus,
battery: provider.battery,
pumpBattery: provider.pumpBattery,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// StorageCurrentGlucoseStateProvider.swift

import Foundation
import HealthKit

/// Reads the latest glucose state from LoopFollow's Storage and Observable layers.
/// This is the only file in the pipeline that is allowed to touch Storage.shared
Expand Down Expand Up @@ -47,6 +48,18 @@ struct StorageCurrentGlucoseStateProvider: CurrentGlucoseStateProviding {
Observable.shared.override.value
}

var overrideEndAt: TimeInterval? {
Observable.shared.overrideEndAt.value
}

var tempTargetMgdl: Double? {
Observable.shared.tempTarget.value?.doubleValue(for: .milligramsPerDeciliter)
}

var tempTargetEndAt: TimeInterval? {
Observable.shared.tempTargetEndAt.value
}

var recBolus: Double? {
Observable.shared.deviceRecBolus.value
}
Expand Down
2 changes: 1 addition & 1 deletion LoopFollow/LiveActivitySettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
get: { slots[index] },
set: { selectSlot($0, at: index) }
)) {
ForEach(LiveActivitySlotOption.allCases, id: \.self) { option in
ForEach(LiveActivitySlotOption.gridCases, id: \.self) { option in
Text(option.displayName).tag(option)
}
}
Expand Down
3 changes: 2 additions & 1 deletion LoopFollow/Remote/LoopAPNS/LoopAPNSRemoteView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import SwiftUI
struct LoopAPNSRemoteView: View {
@Environment(\.presentationMode) var presentationMode
@StateObject private var viewModel = RemoteSettingsViewModel()
@ObservedObject private var activeOverrideNote = Observable.shared.override

var body: some View {
NavigationView {
Expand All @@ -20,7 +21,7 @@ struct LoopAPNSRemoteView: View {
LazyVGrid(columns: columns, spacing: 16) {
CommandButtonView(command: "Meal", iconName: "fork.knife", destination: LoopAPNSCarbsView())
CommandButtonView(command: "Bolus", iconName: "syringe", destination: LoopAPNSBolusView())
CommandButtonView(command: "Overrides", iconName: "slider.horizontal.3", destination: OverridePresetsView())
CommandButtonView(command: "Overrides", iconName: "slider.horizontal.3", destination: OverridePresetsView(), isActive: activeOverrideNote.value != nil)
}
.padding(.horizontal)
} else {
Expand Down
7 changes: 7 additions & 0 deletions LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ struct OverridePresetsView: View {
@StateObject private var viewModel = OverridePresetsViewModel()
@Environment(\.presentationMode) var presentationMode
@ObservedObject var overrideNote = Observable.shared.override
@ObservedObject var overrideEndAt = Observable.shared.overrideEndAt

var body: some View {
NavigationView {
Expand All @@ -28,6 +29,12 @@ struct OverridePresetsView: View {
Text(activeNote)
.font(.subheadline)
.foregroundColor(.secondary)

if let endAt = overrideEndAt.value {
RemainingTimeText(endAt: endAt)
.font(.subheadline)
.foregroundColor(.secondary)
}
}

Spacer()
Expand Down
34 changes: 34 additions & 0 deletions LoopFollow/Remote/RemainingTimeText.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// LoopFollow
// RemainingTimeText.swift

import SwiftUI

/// Remaining time for an active override / temp target: a live countdown when
/// 2 hours or less remain, otherwise a static "29d 23h"-style duration —
/// a multi-day ticker reads poorly.
struct RemainingTimeText: View {
let endAt: TimeInterval

static let tickerMaxRemaining: TimeInterval = 2 * 3600

private static let longFormatter: DateComponentsFormatter = {
let f = DateComponentsFormatter()
f.unitsStyle = .abbreviated
f.allowedUnits = [.day, .hour, .minute]
f.maximumUnitCount = 2
return f
}()

var body: some View {
let end = Date(timeIntervalSince1970: endAt)
let remaining = end.timeIntervalSinceNow
if remaining <= 0 {
EmptyView()
} else if remaining <= Self.tickerMaxRemaining {
Text(timerInterval: Date() ... end, countsDown: true)
.monospacedDigit()
} else {
Text(Self.longFormatter.string(from: remaining) ?? "")
}
}
}
9 changes: 9 additions & 0 deletions LoopFollow/Remote/TRC/OverrideView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ struct OverrideView: View {

@ObservedObject var device = Storage.shared.device
@ObservedObject var overrideNote = Observable.shared.override
@ObservedObject var overrideEndAt = Observable.shared.overrideEndAt

@State private var showAlert: Bool = false
@State private var alertType: AlertType? = nil
Expand Down Expand Up @@ -49,6 +50,14 @@ struct OverrideView: View {
Text(activeNote)
.foregroundColor(.secondary)
}
if let endAt = overrideEndAt.value {
HStack {
Text("Time Remaining")
Spacer()
RemainingTimeText(endAt: endAt)
.foregroundColor(.secondary)
}
}
Button {
alertType = .confirmCancellation
showAlert = true
Expand Down
9 changes: 9 additions & 0 deletions LoopFollow/Remote/TRC/TempTargetView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ struct TempTargetView: View {

@ObservedObject var device = Storage.shared.device
@ObservedObject var tempTarget = Observable.shared.tempTarget
@ObservedObject var tempTargetEndAt = Observable.shared.tempTargetEndAt

@State private var newHKTarget = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 0.0)
@State private var duration = HKQuantity(unit: .minute(), doubleValue: 0.0)
Expand Down Expand Up @@ -51,6 +52,14 @@ struct TempTargetView: View {
Text(Localizer.formatQuantity(tempTargetValue))
Text(Localizer.getPreferredUnit().localizedShortUnitString).foregroundColor(.secondary)
}
if let endAt = tempTargetEndAt.value {
HStack {
Text("Time Remaining")
Spacer()
RemainingTimeText(endAt: endAt)
.foregroundColor(.secondary)
}
}
Button {
alertType = .confirmCancellation
showAlert = true
Expand Down
16 changes: 14 additions & 2 deletions LoopFollow/Remote/TRC/TrioRemoteControlView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import SwiftUI

struct TrioRemoteControlView: View {
@ObservedObject var viewModel: TrioRemoteControlViewModel
@ObservedObject private var activeOverrideNote = Observable.shared.override
@ObservedObject private var activeTempTarget = Observable.shared.tempTarget
@Environment(\.presentationMode) var presentationMode

var body: some View {
Expand All @@ -18,8 +20,8 @@ struct TrioRemoteControlView: View {
LazyVGrid(columns: columns, spacing: 16) {
CommandButtonView(command: "Meal", iconName: "fork.knife", destination: MealView())
CommandButtonView(command: "Bolus", iconName: "syringe", destination: BolusView())
CommandButtonView(command: "Temp Target", iconName: "scope", destination: TempTargetView())
CommandButtonView(command: "Overrides", iconName: "slider.horizontal.3", destination: OverrideView())
CommandButtonView(command: "Temp Target", iconName: "scope", destination: TempTargetView(), isActive: activeTempTarget.value != nil)
CommandButtonView(command: "Overrides", iconName: "slider.horizontal.3", destination: OverrideView(), isActive: activeOverrideNote.value != nil)
}
.padding(.horizontal)

Expand All @@ -34,6 +36,9 @@ struct CommandButtonView<Destination: View>: View {
let command: String
let iconName: String
let destination: Destination
/// Lights the button up with a glow while the corresponding override /
/// temp target is active.
var isActive: Bool = false

var body: some View {
NavigationLink(destination: destination) {
Expand All @@ -49,6 +54,13 @@ struct CommandButtonView<Destination: View>: View {
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
.overlay {
if isActive {
RoundedRectangle(cornerRadius: 8)
.stroke(Color.green, lineWidth: 2.5)
}
}
.shadow(color: isActive ? Color.green.opacity(0.7) : .clear, radius: isActive ? 9 : 0)
}
.buttonStyle(PlainButtonStyle())
}
Expand Down
5 changes: 5 additions & 0 deletions LoopFollow/Storage/Observable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ class Observable {
var tempTarget = ObservableValue<HKQuantity?>(default: nil)
var override = ObservableValue<String?>(default: nil)

/// End of the active override as Unix epoch seconds; nil = no override or indefinite.
var overrideEndAt = ObservableValue<TimeInterval?>(default: nil)
/// End of the active temp target as Unix epoch seconds; nil = no active temp target.
var tempTargetEndAt = ObservableValue<TimeInterval?>(default: nil)

var minAgoText = ObservableValue<String>(default: "?? min ago")
var bgText = ObservableValue<String>(default: "BG")
var bg = ObservableValue<Int?>(default: nil)
Expand Down
Loading
Loading