From b69ae1f88673d526b578fd1db2d7e945c455e056 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 26 Oct 2025 02:32:19 +0000 Subject: [PATCH 1/2] Add pump battery level to Information Display table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add pumpBattery case to InfoType enum with "Pump Battery" display name - Add pumpBatteryLevel to Observable shared storage for quick access - Parse pump.battery.percent from Nightscout API device status - Display pump battery as percentage in Information Display table - Keep existing phone battery display unchanged Follows Nightscout naming conventions and existing code patterns with minimal changes to the codebase. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- LoopFollow/Controllers/Nightscout/DeviceStatus.swift | 11 ++++++++++- LoopFollow/InfoTable/InfoType.swift | 3 ++- LoopFollow/Storage/Observable.swift | 1 + 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift index ac221e445..12e89ebfb 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift @@ -73,7 +73,7 @@ extension MainViewController { // NS Device Status Response Processor func updateDeviceStatusDisplay(jsonDeviceStatus: [[String: AnyObject]]) { - infoManager.clearInfoData(types: [.iob, .cob, .battery, .pump, .target, .isf, .carbRatio, .updated, .recBolus, .tdd]) + infoManager.clearInfoData(types: [.iob, .cob, .battery, .pumpBattery, .pump, .target, .isf, .carbRatio, .updated, .recBolus, .tdd]) // For Loop, clear the current override here - For Trio, it is handled using treatments if Storage.shared.device.value == "Loop" { @@ -105,6 +105,15 @@ extension MainViewController { infoManager.updateInfoData(type: .pump, value: "50+U") } + // Parse pump battery level + if let pumpBattery = lastPumpRecord["battery"] as? [String: AnyObject], + let percent = pumpBattery["percent"] as? Double + { + let pumpBatteryText = String(format: "%.0f", percent) + "%" + infoManager.updateInfoData(type: .pumpBattery, value: pumpBatteryText) + Observable.shared.pumpBatteryLevel.value = percent + } + if let uploader = lastDeviceStatus?["uploader"] as? [String: AnyObject], let upbat = uploader["battery"] as? Double { diff --git a/LoopFollow/InfoTable/InfoType.swift b/LoopFollow/InfoTable/InfoType.swift index 6ea206191..b32fc7be7 100644 --- a/LoopFollow/InfoTable/InfoType.swift +++ b/LoopFollow/InfoTable/InfoType.swift @@ -4,7 +4,7 @@ import Foundation enum InfoType: Int, CaseIterable { - case iob, cob, basal, override, battery, pump, sage, cage, recBolus, minMax, carbsToday, autosens, profile, target, isf, carbRatio, updated, tdd, iage + case iob, cob, basal, override, battery, pumpBattery, pump, sage, cage, recBolus, minMax, carbsToday, autosens, profile, target, isf, carbRatio, updated, tdd, iage var name: String { switch self { @@ -13,6 +13,7 @@ enum InfoType: Int, CaseIterable { case .basal: return "Basal" case .override: return "Override" case .battery: return "Battery" + case .pumpBattery: return "Pump Battery" case .pump: return "Pump" case .sage: return "SAGE" case .cage: return "CAGE" diff --git a/LoopFollow/Storage/Observable.swift b/LoopFollow/Storage/Observable.swift index 992dcc1ff..e3befedee 100644 --- a/LoopFollow/Storage/Observable.swift +++ b/LoopFollow/Storage/Observable.swift @@ -33,6 +33,7 @@ class Observable { var alertLastLoopTime = ObservableValue(default: nil) var deviceRecBolus = ObservableValue(default: nil) var deviceBatteryLevel = ObservableValue(default: nil) + var pumpBatteryLevel = ObservableValue(default: nil) var settingsPath = ObservableValue(default: NavigationPath()) From 5a9699eaaa60c55b70e05f1adb3f2dc00ff25994 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 26 Oct 2025 02:50:32 +0000 Subject: [PATCH 2/2] Add pump battery alarm with configurable threshold MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds a new alarm type that triggers when the pump battery level drops below a user-configurable threshold. The implementation follows the existing battery alarm pattern to minimize new code. Changes: - Add .pumpBattery case to AlarmType enum - Create PumpBatteryCondition evaluator following BatteryCondition pattern - Create PumpBatteryAlarmEditor UI with threshold stepper (0-100%) - Add latestPumpBattery field to AlarmData - Update AlarmTask to pass pump battery data from Observable - Register PumpBatteryCondition in AlarmManager - Add routing to PumpBatteryAlarmEditor in AlarmEditor - Set default threshold to 20% with hour-based snooze - Categorize as device alarm with battery icon and "Pump battery low" description - Register files in Xcode project build system The pump battery data is available via Observable.shared.pumpBatteryLevel from the recent pump battery info display feature. This alarm reuses all existing alarm infrastructure. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- LoopFollow.xcodeproj/project.pbxproj | 8 +++++ LoopFollow/Alarm/Alarm.swift | 7 +++- .../AlarmCondition/PumpBatteryCondition.swift | 16 +++++++++ LoopFollow/Alarm/AlarmData.swift | 1 + .../Alarm/AlarmEditing/AlarmEditor.swift | 1 + .../Editors/PumpBatteryAlarmEditor.swift | 33 +++++++++++++++++++ LoopFollow/Alarm/AlarmManager.swift | 1 + .../Alarm/AlarmType/AlarmType+Snooze.swift | 2 +- .../AlarmType/AlarmType+canAcknowledge.swift | 2 +- LoopFollow/Alarm/AlarmType/AlarmType.swift | 1 + LoopFollow/Task/AlarmTask.swift | 2 ++ 11 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 LoopFollow/Alarm/AlarmCondition/PumpBatteryCondition.swift create mode 100644 LoopFollow/Alarm/AlarmEditing/Editors/PumpBatteryAlarmEditor.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 2871a59de..85ddd0a73 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -190,6 +190,8 @@ DDC7E5CF2DC77C2000EB1127 /* SnoozerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5CE2DC77C2000EB1127 /* SnoozerViewModel.swift */; }; DDCC3A4B2DDBB5E4006F1C10 /* BatteryCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCC3A4A2DDBB5E4006F1C10 /* BatteryCondition.swift */; }; DDCC3A4D2DDBB77C006F1C10 /* BatteryAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCC3A4C2DDBB77C006F1C10 /* BatteryAlarmEditor.swift */; }; + CEAE5A1A975C4AF18ED529CB /* PumpBatteryCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BB6C97B49BC4B52B9F2838B /* PumpBatteryCondition.swift */; }; + E8ECCFAF8161433BB87E672B /* PumpBatteryAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94461A074CD24360A031ACDA /* PumpBatteryAlarmEditor.swift */; }; DDCC3A4F2DDC5B54006F1C10 /* BatteryDropCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCC3A4E2DDC5B54006F1C10 /* BatteryDropCondition.swift */; }; DDCC3A542DDC5D62006F1C10 /* BatteryDropAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCC3A532DDC5D62006F1C10 /* BatteryDropAlarmEditor.swift */; }; DDCC3A562DDC9617006F1C10 /* MissedBolusCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCC3A552DDC9617006F1C10 /* MissedBolusCondition.swift */; }; @@ -582,6 +584,8 @@ DDC7E5CE2DC77C2000EB1127 /* SnoozerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnoozerViewModel.swift; sourceTree = ""; }; DDCC3A4A2DDBB5E4006F1C10 /* BatteryCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryCondition.swift; sourceTree = ""; }; DDCC3A4C2DDBB77C006F1C10 /* BatteryAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryAlarmEditor.swift; sourceTree = ""; }; + 2BB6C97B49BC4B52B9F2838B /* PumpBatteryCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpBatteryCondition.swift; sourceTree = ""; }; + 94461A074CD24360A031ACDA /* PumpBatteryAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpBatteryAlarmEditor.swift; sourceTree = ""; }; DDCC3A4E2DDC5B54006F1C10 /* BatteryDropCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryDropCondition.swift; sourceTree = ""; }; DDCC3A532DDC5D62006F1C10 /* BatteryDropAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryDropAlarmEditor.swift; sourceTree = ""; }; DDCC3A552DDC9617006F1C10 /* MissedBolusCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MissedBolusCondition.swift; sourceTree = ""; }; @@ -839,6 +843,7 @@ DDCC3A552DDC9617006F1C10 /* MissedBolusCondition.swift */, DDCC3A4E2DDC5B54006F1C10 /* BatteryDropCondition.swift */, DDCC3A4A2DDBB5E4006F1C10 /* BatteryCondition.swift */, + 2BB6C97B49BC4B52B9F2838B /* PumpBatteryCondition.swift */, DDB9FC7A2DDB573F00EFAA76 /* IOBCondition.swift */, DDC6CA482DD8E47A0060EE25 /* PumpVolumeCondition.swift */, DDC6CA442DD8D8E60060EE25 /* PumpChangeCondition.swift */, @@ -1102,6 +1107,7 @@ DDCC3A572DDC9655006F1C10 /* MissedBolusAlarmEditor.swift */, DDCC3A532DDC5D62006F1C10 /* BatteryDropAlarmEditor.swift */, DDCC3A4C2DDBB77C006F1C10 /* BatteryAlarmEditor.swift */, + 94461A074CD24360A031ACDA /* PumpBatteryAlarmEditor.swift */, DDB9FC7C2DDB575300EFAA76 /* IOBAlarmEditor.swift */, DDC6CA4A2DD8E4960060EE25 /* PumpVolumeAlarmEditor.swift */, DDC6CA462DD8D9010060EE25 /* PumpChangeAlarmEditor.swift */, @@ -1920,6 +1926,7 @@ DDC7E5152DBCFA7900EB1127 /* SnoozerViewController.swift in Sources */, DD7F4C072DD5042F00D449E9 /* OverrideStartAlarmEditor.swift in Sources */, DDCC3A4B2DDBB5E4006F1C10 /* BatteryCondition.swift in Sources */, + CEAE5A1A975C4AF18ED529CB /* PumpBatteryCondition.swift in Sources */, DDDF6F492D479AF000884336 /* NoRemoteView.swift in Sources */, 656F8C142E49F3D20008DC1D /* RemoteCommandSettings.swift in Sources */, DD12D4872E1705E6004E0112 /* AlarmsContainerView.swift in Sources */, @@ -2021,6 +2028,7 @@ DD13BC772C3FD64E0062313B /* InfoData.swift in Sources */, DD13BC752C3FD6210062313B /* InfoType.swift in Sources */, DDCC3A4D2DDBB77C006F1C10 /* BatteryAlarmEditor.swift in Sources */, + E8ECCFAF8161433BB87E672B /* PumpBatteryAlarmEditor.swift in Sources */, DDC6CA492DD8E47A0060EE25 /* PumpVolumeCondition.swift in Sources */, DD1A97142D4294A5000DDC11 /* AdvancedSettingsView.swift in Sources */, DD9ACA0A2D33095600415D8A /* MinAgoTask.swift in Sources */, diff --git a/LoopFollow/Alarm/Alarm.swift b/LoopFollow/Alarm/Alarm.swift index 08dfc2cb9..dadaf75d8 100644 --- a/LoopFollow/Alarm/Alarm.swift +++ b/LoopFollow/Alarm/Alarm.swift @@ -249,6 +249,9 @@ struct Alarm: Identifiable, Codable, Equatable { soundFile = .machineCharge delta = 10 monitoringWindow = 15 + case .pumpBattery: + soundFile = .machineCharge + threshold = 20 case .recBolus: soundFile = .dholShuffleloop threshold = 1 @@ -287,7 +290,7 @@ extension AlarmType { return .glucose case .iob, .cob, .missedBolus, .recBolus: return .insulin - case .battery, .batteryDrop, .pump, .pumpChange, + case .battery, .batteryDrop, .pumpBattery, .pump, .pumpChange, .sensorChange, .notLooping, .buildExpire: return .device case .overrideStart, .overrideEnd, .tempTargetStart, .tempTargetEnd: @@ -308,6 +311,7 @@ extension AlarmType { case .recBolus: return "bolt.horizontal" case .battery: return "battery.25" case .batteryDrop: return "battery.100.bolt" + case .pumpBattery: return "battery.25" case .pump: return "drop" case .pumpChange: return "arrow.triangle.2.circlepath" case .sensorChange: return "sensor.tag.radiowaves.forward" @@ -334,6 +338,7 @@ extension AlarmType { case .recBolus: return "Recommended bolus issued." case .battery: return "Phone battery low." case .batteryDrop: return "Battery drops quickly." + case .pumpBattery: return "Pump battery low." case .pump: return "Reservoir level low." case .pumpChange: return "Pump change due." case .sensorChange: return "Sensor change due." diff --git a/LoopFollow/Alarm/AlarmCondition/PumpBatteryCondition.swift b/LoopFollow/Alarm/AlarmCondition/PumpBatteryCondition.swift new file mode 100644 index 000000000..fe3e5828e --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/PumpBatteryCondition.swift @@ -0,0 +1,16 @@ +// LoopFollow +// PumpBatteryCondition.swift + +import Foundation + +struct PumpBatteryCondition: AlarmCondition { + static let type: AlarmType = .pumpBattery + init() {} + + func evaluate(alarm: Alarm, data: AlarmData, now _: Date) -> Bool { + guard let limit = alarm.threshold, limit > 0 else { return false } + guard let level = data.latestPumpBattery else { return false } + + return level <= limit + } +} diff --git a/LoopFollow/Alarm/AlarmData.swift b/LoopFollow/Alarm/AlarmData.swift index 8958c76d6..b960a9223 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 latestPumpBattery: Double? let batteryHistory: [DataStructs.batteryStruct] let recentCarbs: [CarbSample] } diff --git a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift index 145d345a1..48a1b7196 100644 --- a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift @@ -80,6 +80,7 @@ struct AlarmEditor: View { case .iob: IOBAlarmEditor(alarm: $alarm) case .battery: BatteryAlarmEditor(alarm: $alarm) case .batteryDrop: BatteryDropAlarmEditor(alarm: $alarm) + case .pumpBattery: PumpBatteryAlarmEditor(alarm: $alarm) case .missedBolus: MissedBolusAlarmEditor(alarm: $alarm) } } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/PumpBatteryAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/PumpBatteryAlarmEditor.swift new file mode 100644 index 000000000..39d44809f --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/PumpBatteryAlarmEditor.swift @@ -0,0 +1,33 @@ +// LoopFollow +// PumpBatteryAlarmEditor.swift + +import SwiftUI + +struct PumpBatteryAlarmEditor: View { + @Binding var alarm: Alarm + + var body: some View { + Group { + InfoBanner( + text: "This warns you when the pump's battery gets low, based on the percentage you choose.", + alarmType: alarm.type + ) + + AlarmGeneralSection(alarm: $alarm) + + AlarmStepperSection( + header: "Pump Battery Level", + footer: "This alerts you when the pump battery drops below this level.", + title: "Pump Battery Below", + range: 0 ... 100, + step: 5, + unitLabel: "%", + value: $alarm.threshold + ) + + AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm) + AlarmSnoozeSection(alarm: $alarm) + } + } +} diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift index 09ff8d610..99a2a0620 100644 --- a/LoopFollow/Alarm/AlarmManager.swift +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -32,6 +32,7 @@ class AlarmManager { IOBCondition.self, BatteryCondition.self, BatteryDropCondition.self, + PumpBatteryCondition.self, ] ) { var dict = [AlarmType: AlarmCondition]() diff --git a/LoopFollow/Alarm/AlarmType/AlarmType+Snooze.swift b/LoopFollow/Alarm/AlarmType/AlarmType+Snooze.swift index 09e183491..e369e7e06 100644 --- a/LoopFollow/Alarm/AlarmType/AlarmType+Snooze.swift +++ b/LoopFollow/Alarm/AlarmType/AlarmType+Snooze.swift @@ -15,7 +15,7 @@ extension AlarmType { .overrideStart, .overrideEnd, .tempTargetStart, .tempTargetEnd: return .minute - case .battery, .batteryDrop, .sensorChange, .pumpChange, .cob, .iob, + case .battery, .batteryDrop, .pumpBattery, .sensorChange, .pumpChange, .cob, .iob, .pump: return .hour case .temporary: diff --git a/LoopFollow/Alarm/AlarmType/AlarmType+canAcknowledge.swift b/LoopFollow/Alarm/AlarmType/AlarmType+canAcknowledge.swift index c5643ec4b..5283d03b2 100644 --- a/LoopFollow/Alarm/AlarmType/AlarmType+canAcknowledge.swift +++ b/LoopFollow/Alarm/AlarmType/AlarmType+canAcknowledge.swift @@ -12,7 +12,7 @@ extension AlarmType { return true // These are alarms without memory, if they only are acknowledged - they would alarm again immediately case - .batteryDrop, .missedReading, .notLooping, .battery, .buildExpire, .iob, .sensorChange, .pumpChange, .pump: + .batteryDrop, .pumpBattery, .missedReading, .notLooping, .battery, .buildExpire, .iob, .sensorChange, .pumpChange, .pump: return false } } diff --git a/LoopFollow/Alarm/AlarmType/AlarmType.swift b/LoopFollow/Alarm/AlarmType/AlarmType.swift index e298ef812..0948808b5 100644 --- a/LoopFollow/Alarm/AlarmType/AlarmType.swift +++ b/LoopFollow/Alarm/AlarmType/AlarmType.swift @@ -21,6 +21,7 @@ enum AlarmType: String, CaseIterable, Codable { case pump = "Pump Insulin Alert" case battery = "Low Battery" case batteryDrop = "Battery Drop" + case pumpBattery = "Low Pump Battery" case recBolus = "Rec. Bolus" case overrideStart = "Override Started" case overrideEnd = "Override Ended" diff --git a/LoopFollow/Task/AlarmTask.swift b/LoopFollow/Task/AlarmTask.swift index 686916042..0102d66ed 100644 --- a/LoopFollow/Task/AlarmTask.swift +++ b/LoopFollow/Task/AlarmTask.swift @@ -26,6 +26,7 @@ extension MainViewController { let latestPumpVol = self.latestPumpVolume let bolusEntries = self.bolusData.map { BolusEntry(units: $0.value, date: Date(timeIntervalSince1970: $0.date)) } let latestBattery = Observable.shared.deviceBatteryLevel.value + let latestPumpBattery = Observable.shared.pumpBatteryLevel.value let recentCarbs: [CarbSample] = self.carbData.map { CarbSample(grams: $0.value, date: Date(timeIntervalSince1970: $0.date)) } let alarmData = AlarmData( @@ -49,6 +50,7 @@ extension MainViewController { IOB: self.latestIOB?.value, recentBoluses: bolusEntries, latestBattery: latestBattery, + latestPumpBattery: latestPumpBattery, batteryHistory: self.deviceBatteryData, recentCarbs: recentCarbs )