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
45 changes: 45 additions & 0 deletions Localization/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -6489,6 +6489,9 @@
}
}
},
"1 Year" : {
"comment" : "Title string for PodSessionLogRetention.days365"
},
"2 hours" : {
"comment" : "Button text for 2 hour suspend duration",
"extractionState" : "manual",
Expand Down Expand Up @@ -6651,6 +6654,9 @@
}
}
},
"30 Days" : {
"comment" : "Title string for PodSessionLogRetention.days30"
},
"30 minutes" : {
"comment" : "Button text for 30 minute suspend duration",
"extractionState" : "manual",
Expand Down Expand Up @@ -6813,6 +6819,12 @@
}
}
},
"90 Days" : {
"comment" : "Title string for PodSessionLogRetention.days90"
},
"180 Days" : {
"comment" : "Title string for PodSessionLogRetention.days180"
},
"A new certificate will be used for the next Omnipod 5 Pod pairing." : {
"comment" : "Confirmation message when a new certificate will be used"
},
Expand Down Expand Up @@ -19477,6 +19489,9 @@
}
}
},
"Each completed pod is recorded here with its fault code and ref string, if any, so you can report it to your supplier without needing a screenshot." : {
"comment" : "Footer explanation for the pod session log"
},
"Downloaded" : {
"comment" : "O5 cert source: downloaded"
},
Expand Down Expand Up @@ -24508,6 +24523,12 @@
}
}
},
"Fault %1$@ · %2$@" : {
"comment" : "Format string for pod session log row summary: (1: fault code) (2: fault description)"
},
"Fault %1$@ · Ref %2$@" : {
"comment" : "Format string for pod session log row summary with a ref string: (1: fault code) (2: pdm ref string)"
},
"Fault event occurred" : {
"comment" : "Pod state when fault event has occurred",
"extractionState" : "manual",
Expand Down Expand Up @@ -26290,6 +26311,9 @@
}
}
},
"Forever" : {
"comment" : "Title string for PodSessionLogRetention.forever"
},
"Generating App Attest key…" : {
"comment" : "O5 fetch progress: generate key"
},
Expand Down Expand Up @@ -30997,6 +31021,9 @@
}
}
},
"Keep Log For" : {
"comment" : "Label for pod session log retention picker"
},
"Keep the RileyLink about 6 inches from the pod during pairing." : {
"comment" : "Label text for step 2 of Eros pair pod instructions",
"extractionState" : "manual",
Expand Down Expand Up @@ -31645,6 +31672,9 @@
}
}
},
"Log Settings" : {
"comment" : "Section header for pod session log settings"
},
"Load custom certificate" : {
"comment" : "Menu action to import an o5keypair file"
},
Expand Down Expand Up @@ -36508,6 +36538,9 @@
}
}
},
"No fault" : {
"comment" : "Pod session log row summary when the pod had no fault"
},
"No faults" : {
"comment" : "Description for Fault Event Code .noFaults",
"extractionState" : "manual",
Expand Down Expand Up @@ -37156,6 +37189,9 @@
}
}
},
"No pod sessions recorded yet." : {
"comment" : "Text shown when the pod session log is empty"
},
"No pods found" : {
"comment" : "Error message for PodCommsError.noPodsFound",
"extractionState" : "manual",
Expand Down Expand Up @@ -50616,6 +50652,12 @@
}
}
},
"Pod Session Log" : {
"comment" : "Navigation title for the pod session log screen\nText for Pod Session Log row and page"
},
"Pod Sessions" : {
"comment" : "Section header for pod session log entries"
},
"Pod Setup" : {
"comment" : "Title for PodSetupView",
"extractionState" : "manual",
Expand Down Expand Up @@ -72052,6 +72094,9 @@
}
}
},
"Unknown Date" : {
"comment" : "Pod session log row date when no date is available"
},
"Unknown Omnipod Pod Type" : {
"comment" : "Description for an unknown Omnipod pod type",
"extractionState" : "manual",
Expand Down
55 changes: 55 additions & 0 deletions OmnipodKit/OmnipodCommon/PodSessionLogRetention.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//
// PodSessionLogRetention.swift
// OmnipodKit
//
// Created for the pod session log feature.
// Copyright © 2026 LoopKit Authors. All rights reserved.
//

import Foundation

/// How long completed pod sessions are kept in the pod session log before
/// being pruned. Each pod session (normal deactivation, expiration, or fault)
/// is recorded with its activation/end dates and full pod status, including
/// fault code and PDM ref string when applicable, so it can be reviewed later
/// or reported to the pod manufacturer without relying on screenshots.
enum PodSessionLogRetention: Int, CaseIterable, Codable {
case days30
case days90
case days180
case days365
case forever

static let `default`: PodSessionLogRetention = .days90

/// Number of days to retain entries, or nil to keep the log forever.
var days: Int? {
switch self {
case .days30:
return 30
case .days90:
return 90
case .days180:
return 180
case .days365:
return 365
case .forever:
return nil
}
}

var title: String {
switch self {
case .days30:
return LocalizedString("30 Days", comment: "Title string for PodSessionLogRetention.days30")
case .days90:
return LocalizedString("90 Days", comment: "Title string for PodSessionLogRetention.days90")
case .days180:
return LocalizedString("180 Days", comment: "Title string for PodSessionLogRetention.days180")
case .days365:
return LocalizedString("1 Year", comment: "Title string for PodSessionLogRetention.days365")
case .forever:
return LocalizedString("Forever", comment: "Title string for PodSessionLogRetention.forever")
}
}
}
36 changes: 36 additions & 0 deletions OmnipodKit/PumpManager/OmniPumpManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,39 @@ extension OmniPumpManager {
}
}

// Completed pod sessions currently retained in the session log, most recent first.
var podSessionLog: [PodState] {
state.podSessionLog
}

// Deletes specific pod session log entries (e.g. via swipe-to-delete in the UI).
func deletePodSessionLogEntries(at offsets: IndexSet) {
setState { state in
state.removeSessionLogEntries(at: offsets)
}
}

// Clears the entire pod session log.
func clearPodSessionLog() {
setState { state in
state.clearSessionLog()
}
}

var podSessionLogRetention: PodSessionLogRetention {
set {
setState { (state) in
state.podSessionLogRetention = newValue
// Apply the new retention window immediately rather than waiting
// for the next pod session to end.
state.pruneSessionLog()
}
}
get {
state.podSessionLogRetention
}
}

var defaultLowReservoirReminderValue: Double {
set {
setState { (state) in
Expand Down Expand Up @@ -1035,6 +1068,9 @@ extension OmniPumpManager {
if podState.deliveryStoppedAt == nil {
state.previousPodState?.deliveryStoppedAt = Date()
}
if let completedPodState = state.previousPodState {
state.recordCompletedPodSession(completedPodState)
}
}

switch podType {
Expand Down
55 changes: 55 additions & 0 deletions OmnipodKit/PumpManager/OmniPumpManagerState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ public struct OmniPumpManagerState: RawRepresentable, Equatable {
// for user review and manufacturer reporting.
internal var previousPodState: PodState?

// Rolling log of completed pod sessions (deactivated, expired, or faulted),
// most recent first, for user review and manufacturer reporting without
// needing to rely on screenshots. Pruned according to podSessionLogRetention.
internal var podSessionLog: [PodState] = []

// How long completed pod sessions are kept in podSessionLog before being pruned.
var podSessionLogRetention: PodSessionLogRetention = .default

// Indicates that the user has completed initial configuration
// which means they have configured any parameters, but may not have paired a pod yet.
var initialConfigurationCompleted: Bool = false
Expand Down Expand Up @@ -347,6 +355,19 @@ public struct OmniPumpManagerState: RawRepresentable, Equatable {
self.previousPodState = nil
}

if let rawSessionLog = rawValue["podSessionLog"] as? [PodState.RawValue] {
self.podSessionLog = rawSessionLog.compactMap { PodState(rawValue: $0) }
} else {
self.podSessionLog = []
}

if let rawRetention = rawValue["podSessionLogRetention"] as? Int,
let retention = PodSessionLogRetention(rawValue: rawRetention) {
self.podSessionLogRetention = retention
} else {
self.podSessionLogRetention = .default
}

if podType.isEros {
// Some more Eros specific values
if let pairingAttemptAddress = rawValue["pairingAttemptAddress"] as? UInt32 {
Expand Down Expand Up @@ -387,12 +408,44 @@ public struct OmniPumpManagerState: RawRepresentable, Equatable {
value["lastPumpDataReportDate"] = lastPumpDataReportDate
value["silencePodEnd"] = silencePodEnd
value["previousPodState"] = previousPodState?.rawValue
value["podSessionLog"] = podSessionLog.map { $0.rawValue }
value["podSessionLogRetention"] = podSessionLogRetention.rawValue

return value
}
}

extension OmniPumpManagerState {
// Records a just-completed pod session (call once, when a pod session ends)
// and prunes the log per the current retention setting.
mutating func recordCompletedPodSession(_ podState: PodState) {
podSessionLog.insert(podState, at: 0)
pruneSessionLog()
}

// Removes podSessionLog entries older than podSessionLogRetention allows.
// Safe to call any time, e.g. right after the user changes the retention setting.
mutating func pruneSessionLog() {
guard let retentionDays = podSessionLogRetention.days else {
return // .forever: keep everything
}
let cutoff = Date().addingTimeInterval(-.days(Double(retentionDays)))
podSessionLog.removeAll { session in
let sessionEndDate = session.deliveryStoppedAt ?? session.activatedAt ?? .distantPast
return sessionEndDate < cutoff
}
}

// Removes specific podSessionLog entries, e.g. from a swipe-to-delete action.
mutating func removeSessionLogEntries(at offsets: IndexSet) {
podSessionLog.remove(atOffsets: offsets)
}

// Empties the entire podSessionLog, e.g. from a "Clear Log" action.
mutating func clearSessionLog() {
podSessionLog.removeAll()
}

var hasActivePod: Bool {
return podState?.isActive == true
}
Expand Down Expand Up @@ -437,6 +490,8 @@ extension OmniPumpManagerState: CustomDebugStringConvertible {
"* acknowledgedTimeOffsetAlert: \(acknowledgedTimeOffsetAlert)",
"* initialConfigurationCompleted: \(initialConfigurationCompleted)",
"* podType: \(podType)",
"* podSessionLogRetention: \(podSessionLogRetention)",
"* podSessionLog: \(podSessionLog.count) session(s)",
"",
].joined(separator: "\n")
if podType.usesRileyLink {
Expand Down
31 changes: 31 additions & 0 deletions OmnipodKit/PumpManagerUI/ViewModels/OmniSettingsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,17 @@ class OmniSettingsViewModel: ObservableObject {
@Published var previousPodDetails: PodDetails?

@Published var controllerId: UInt32

@Published var podSessionLogDetails: [PodDetails] = []

@Published var podSessionLogRetention: PodSessionLogRetention {
didSet {
self.pumpManager.podSessionLogRetention = podSessionLogRetention
// Changing retention may prune entries; refresh what's shown right away.
self.podSessionLogDetails = self.pumpManager.podSessionLogDetails
}
}


var timeZone: TimeZone {
return pumpManager.status.timeZone
Expand Down Expand Up @@ -276,6 +287,8 @@ class OmniSettingsViewModel: ObservableObject {
podDetails = pumpManager.podDetails
previousPodDetails = pumpManager.previousPodDetails
controllerId = pumpManager.state.controllerId
podSessionLogDetails = pumpManager.podSessionLogDetails
podSessionLogRetention = pumpManager.podSessionLogRetention

pumpManager.addPodStateObserver(self, queue: DispatchQueue.main)
pumpManager.addStatusObserver(self, queue: DispatchQueue.main)
Expand Down Expand Up @@ -432,6 +445,16 @@ class OmniSettingsViewModel: ObservableObject {
self.podKeepAlivePreference = podKeepAlivePreference
}

func deletePodSessionLogEntries(at offsets: IndexSet) {
pumpManager.deletePodSessionLogEntries(at: offsets)
self.podSessionLogDetails = pumpManager.podSessionLogDetails
}

func clearPodSessionLog() {
pumpManager.clearPodSessionLog()
self.podSessionLogDetails = pumpManager.podSessionLogDetails
}

func didChangeInsulinType(_ newType: InsulinType?) {
self.pumpManager.insulinType = newType
}
Expand Down Expand Up @@ -586,6 +609,7 @@ extension OmniSettingsViewModel: PodStateObserver {
insulinType = self.pumpManager.insulinType
podDetails = self.pumpManager.podDetails
previousPodDetails = self.pumpManager.previousPodDetails
podSessionLogDetails = self.pumpManager.podSessionLogDetails
}

func podConnectionStateDidChange(isConnected: Bool) {
Expand Down Expand Up @@ -693,6 +717,13 @@ extension OmniPumpManager {
return podDetails(fromPodState: podState, andDeviceName: nil)
}

// Completed pod sessions currently retained in the session log, most recent first,
// for display in PodSessionLogView. Tapping an entry reuses PodDetailsView, same as
// the single-slot "Previous Pod" screen.
var podSessionLogDetails: [PodDetails] {
return state.podSessionLog.map { podDetails(fromPodState: $0, andDeviceName: nil) }
}

}

extension OmniPumpManager: DiagnosticCommands {
Expand Down
Loading