From 2a5ad66b16654847f13566d862ed6f1709fc2a2a Mon Sep 17 00:00:00 2001 From: bastiaanv Date: Sat, 9 Aug 2025 14:02:27 +0200 Subject: [PATCH 01/73] feat: add MedtrumKit --- .gitmodules | 4 ++++ EversenseKit | 1 + MedtrumKit | 1 + Trio.xcodeproj/project.pbxproj | 6 ++++++ Trio.xcworkspace/contents.xcworkspacedata | 3 +++ Trio/Resources/InfoPlist.xcstrings | 4 ++-- Trio/Sources/APS/DeviceDataManager.swift | 10 ++++++++++ Trio/Sources/Localizations/Main/Localizable.xcstrings | 6 ++++++ Trio/Sources/Modules/Home/View/HomeRootView.swift | 1 + .../Modules/Onboarding/OnboardingStateModel.swift | 5 +++++ .../Modules/Onboarding/View/OnboardingRootView.swift | 3 ++- .../AlgorithmSettingsSubstepView.swift | 3 ++- .../Modules/Onboarding/View/OnboardingView+Util.swift | 3 +++ .../Modules/PumpConfig/PumpConfigDataFlow.swift | 1 + .../Modules/PumpConfig/View/PumpConfigRootView.swift | 5 +++++ .../Modules/PumpConfig/View/PumpSetupView.swift | 10 ++++++++++ 16 files changed, 62 insertions(+), 4 deletions(-) create mode 160000 EversenseKit create mode 160000 MedtrumKit diff --git a/.gitmodules b/.gitmodules index ca2bf7849f1..93412e5a983 100644 --- a/.gitmodules +++ b/.gitmodules @@ -41,3 +41,7 @@ [submodule "DanaKit"] path = DanaKit url = https://github.com/loopandlearn/DanaKit +[submodule "MedtrumKit"] + path = MedtrumKit + branch = dev + url = https://github.com/loopandlearn/MedtrumKit diff --git a/EversenseKit b/EversenseKit new file mode 160000 index 00000000000..851095c569f --- /dev/null +++ b/EversenseKit @@ -0,0 +1 @@ +Subproject commit 851095c569f3673cae40358c75b10bb9ba0b6330 diff --git a/MedtrumKit b/MedtrumKit new file mode 160000 index 00000000000..5bfb80667c6 --- /dev/null +++ b/MedtrumKit @@ -0,0 +1 @@ +Subproject commit 5bfb80667c6f7f53381d07822c89dad4ee0ac60c diff --git a/Trio.xcodeproj/project.pbxproj b/Trio.xcodeproj/project.pbxproj index 7210d2a5ba5..a21af680bee 100644 --- a/Trio.xcodeproj/project.pbxproj +++ b/Trio.xcodeproj/project.pbxproj @@ -256,6 +256,8 @@ 3BD6CE262DC24CFD00FA0472 /* pumphistory-24h-zoned.json in Resources */ = {isa = PBXBuildFile; fileRef = 3BD6CE252DC24CFD00FA0472 /* pumphistory-24h-zoned.json */; }; 3BD9687C2D8DDD4600899469 /* SlideButton in Frameworks */ = {isa = PBXBuildFile; productRef = 3BD9687B2D8DDD4600899469 /* SlideButton */; }; 3BD9687F2D8DDD8800899469 /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 3BD9687E2D8DDD8800899469 /* CryptoSwift */; }; + 3E54EF2C2E476DA40006F54D /* MedtrumKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3E54EF2B2E476DA40006F54D /* MedtrumKit.framework */; }; + 3E54EF2D2E476DA40006F54D /* MedtrumKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3E54EF2B2E476DA40006F54D /* MedtrumKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 45252C95D220E796FDB3B022 /* ConfigEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F8A87AA037BD079BA3528BA /* ConfigEditorDataFlow.swift */; }; 45717281F743594AA9D87191 /* ConfigEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 920DDB21E5D0EB813197500D /* ConfigEditorRootView.swift */; }; 491D6FBD2D56741C00C49F67 /* TempTargetStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FBC2D56741C00C49F67 /* TempTargetStored+CoreDataProperties.swift */; }; @@ -774,6 +776,7 @@ 3B4BA7792D8DBD690069D5B8 /* MinimedKit.framework in Embed Frameworks */, CE95BF5C2BA770C300DC3DE3 /* LoopKit.framework in Embed Frameworks */, 3B4BA7712D8DBD690069D5B8 /* G7SensorKit.framework in Embed Frameworks */, + 3E54EF2D2E476DA40006F54D /* MedtrumKit.framework in Embed Frameworks */, CEB434FE28B90B8C00B70274 /* SwiftCharts in Embed Frameworks */, 3B4BA7812D8DBD690069D5B8 /* OmniKitUI.framework in Embed Frameworks */, 3B4BA76F2D8DBD690069D5B8 /* DanaKit.framework in Embed Frameworks */, @@ -1074,6 +1077,7 @@ 3BD6CE252DC24CFD00FA0472 /* pumphistory-24h-zoned.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "pumphistory-24h-zoned.json"; sourceTree = ""; }; 3BDEA2DC60EDE0A3CA54DC73 /* TargetsEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorProvider.swift; sourceTree = ""; }; 3BF768BD6264FF7D71D66767 /* NightscoutConfigProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NightscoutConfigProvider.swift; sourceTree = ""; }; + 3E54EF2B2E476DA40006F54D /* MedtrumKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MedtrumKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3F60E97100041040446F44E7 /* PumpConfigStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpConfigStateModel.swift; sourceTree = ""; }; 3F8A87AA037BD079BA3528BA /* ConfigEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConfigEditorDataFlow.swift; sourceTree = ""; }; 42369F66CF91F30624C0B3A6 /* BasalProfileEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorProvider.swift; sourceTree = ""; }; @@ -1559,6 +1563,7 @@ 3BD9687C2D8DDD4600899469 /* SlideButton in Frameworks */, 3B4BA7782D8DBD690069D5B8 /* MinimedKit.framework in Frameworks */, 3B4BA7762D8DBD690069D5B8 /* LibreTransmitterUI.framework in Frameworks */, + 3E54EF2C2E476DA40006F54D /* MedtrumKit.framework in Frameworks */, 3B4BA7902D8DC0EC0069D5B8 /* TidepoolServiceKitUI.framework in Frameworks */, 3B4BA76A2D8DBD690069D5B8 /* CGMBLEKit.framework in Frameworks */, 3B4BA77C2D8DBD690069D5B8 /* OmniBLE.framework in Frameworks */, @@ -2161,6 +2166,7 @@ 3818AA48274C267000843DB3 /* Frameworks */ = { isa = PBXGroup; children = ( + 3E54EF2B2E476DA40006F54D /* MedtrumKit.framework */, 3B4BA7882D8DC0EC0069D5B8 /* TidepoolServiceKit.framework */, 3B4BA7892D8DC0EC0069D5B8 /* TidepoolServiceKitUI.framework */, 3B4BA75B2D8DBD690069D5B8 /* CGMBLEKit.framework */, diff --git a/Trio.xcworkspace/contents.xcworkspacedata b/Trio.xcworkspace/contents.xcworkspacedata index 8035089c5cc..5c7c148fc8c 100644 --- a/Trio.xcworkspace/contents.xcworkspacedata +++ b/Trio.xcworkspace/contents.xcworkspacedata @@ -16,6 +16,9 @@ + + diff --git a/Trio/Resources/InfoPlist.xcstrings b/Trio/Resources/InfoPlist.xcstrings index 82e1dead5a0..e8388f2cc93 100644 --- a/Trio/Resources/InfoPlist.xcstrings +++ b/Trio/Resources/InfoPlist.xcstrings @@ -8,7 +8,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "Trio" + "value" : "Trio Medtrum" } } } @@ -20,7 +20,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "Trio" + "value" : "Trio Medtrum" } } } diff --git a/Trio/Sources/APS/DeviceDataManager.swift b/Trio/Sources/APS/DeviceDataManager.swift index 7d487be917a..cf82b520c3f 100644 --- a/Trio/Sources/APS/DeviceDataManager.swift +++ b/Trio/Sources/APS/DeviceDataManager.swift @@ -5,6 +5,7 @@ import DanaKit import Foundation import LoopKit import LoopKitUI +import MedtrumKit import MinimedKit import MockKit import OmniBLE @@ -36,6 +37,7 @@ private let staticPumpManagers: [PumpManagerUI.Type] = [ OmnipodPumpManager.self, OmniBLEPumpManager.self, DanaKitPumpManager.self, + MedtrumPumpManager.self, MockPumpManager.self ] @@ -44,6 +46,7 @@ private let staticPumpManagersByIdentifier: [String: PumpManagerUI.Type] = [ OmnipodPumpManager.pluginIdentifier: OmnipodPumpManager.self, OmniBLEPumpManager.pluginIdentifier: OmniBLEPumpManager.self, DanaKitPumpManager.pluginIdentifier: DanaKitPumpManager.self, + MedtrumPumpManager.pluginIdentifier: MedtrumPumpManager.self, MockPumpManager.pluginIdentifier: MockPumpManager.self ] @@ -111,6 +114,13 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable { } pumpExpiresAtDate.send(endTime) } + if let medtrum = pumpManager as? MedtrumPumpManager { + guard let endTime = medtrum.state.patchExpiresAt else { + pumpExpiresAtDate.send(nil) + return + } + pumpExpiresAtDate.send(endTime) + } if let simulatorPump = pumpManager as? MockPumpManager { pumpDisplayState.value = PumpDisplayState(name: simulatorPump.localizedTitle, image: simulatorPump.smallImage) pumpName.send(simulatorPump.localizedTitle) diff --git a/Trio/Sources/Localizations/Main/Localizable.xcstrings b/Trio/Sources/Localizations/Main/Localizable.xcstrings index 36624e46d9e..2ea976237fe 100644 --- a/Trio/Sources/Localizations/Main/Localizable.xcstrings +++ b/Trio/Sources/Localizations/Main/Localizable.xcstrings @@ -14040,6 +14040,9 @@ } } } + }, + "• Medtrum Nano (200u/300u)" : { + }, "• Nightscout" : { "localizations" : { @@ -140428,6 +140431,9 @@ } } } + }, + "Medtrum Nano" : { + }, "Menu" : { "comment" : "Menu", diff --git a/Trio/Sources/Modules/Home/View/HomeRootView.swift b/Trio/Sources/Modules/Home/View/HomeRootView.swift index 432560e4a7a..fb700f896d8 100644 --- a/Trio/Sources/Modules/Home/View/HomeRootView.swift +++ b/Trio/Sources/Modules/Home/View/HomeRootView.swift @@ -951,6 +951,7 @@ extension Home { Button("Omnipod Eros") { state.addPump(.omnipod) } Button("Omnipod DASH") { state.addPump(.omnipodBLE) } Button("Dana(RS/-i)") { state.addPump(.dana) } + Button("Medtrum Nano") { state.addPump(.medtrum) } Button("Pump Simulator") { state.addPump(.simulator) } } message: { Text("Select Pump Model") } .sheet(isPresented: $state.shouldDisplayPumpSetupSheet) { diff --git a/Trio/Sources/Modules/Onboarding/OnboardingStateModel.swift b/Trio/Sources/Modules/Onboarding/OnboardingStateModel.swift index 04120769c94..b232f1a1016 100644 --- a/Trio/Sources/Modules/Onboarding/OnboardingStateModel.swift +++ b/Trio/Sources/Modules/Onboarding/OnboardingStateModel.swift @@ -3,6 +3,7 @@ import DanaKit import FirebaseCrashlytics import Foundation import LoopKit +import MedtrumKit import MinimedKit import Observation import OmniBLE @@ -118,6 +119,8 @@ extension Onboarding { defaultOption = .omnipodDash } else if pumpManager is OmnipodPumpManager { defaultOption = .omnipodEros + } else if pumpManager is MedtrumPumpManager { + defaultOption = .medtrum } else if pumpManager is DanaKitPumpManager { defaultOption = .dana } else if pumpManager is MinimedPumpManager { @@ -163,6 +166,8 @@ extension Onboarding { return PickerSetting(value: 0.1, step: 0.05, min: 0, max: 30, type: .insulinUnitPerHour) case .omnipodEros: return PickerSetting(value: 0.1, step: 0.05, min: 0.05, max: 30, type: .insulinUnitPerHour) + case .medtrum: + return PickerSetting(value: 0.1, step: 0.05, min: 0.05, max: 30, type: .insulinUnitPerHour) case .none: // same as dash, as that is the fallback return PickerSetting(value: 0.1, step: 0.05, min: 0, max: 30, type: .insulinUnitPerHour) diff --git a/Trio/Sources/Modules/Onboarding/View/OnboardingRootView.swift b/Trio/Sources/Modules/Onboarding/View/OnboardingRootView.swift index 38578de7bf4..a7001c3cbd9 100644 --- a/Trio/Sources/Modules/Onboarding/View/OnboardingRootView.swift +++ b/Trio/Sources/Modules/Onboarding/View/OnboardingRootView.swift @@ -593,7 +593,8 @@ struct OnboardingNavigationButtons: View { case .dana, .minimed: currentAutosensSubstep = .rewindResetsAutosens - case .omnipodDash, + case .medtrum, + .omnipodDash, .omnipodEros: currentAutosensSubstep = .autosensMax } diff --git a/Trio/Sources/Modules/Onboarding/View/OnboardingSteps/AlgorithmSettings/AlgorithmSettingsSubstepView.swift b/Trio/Sources/Modules/Onboarding/View/OnboardingSteps/AlgorithmSettings/AlgorithmSettingsSubstepView.swift index a9ef4024dbc..da259b06f64 100644 --- a/Trio/Sources/Modules/Onboarding/View/OnboardingSteps/AlgorithmSettings/AlgorithmSettingsSubstepView.swift +++ b/Trio/Sources/Modules/Onboarding/View/OnboardingSteps/AlgorithmSettings/AlgorithmSettingsSubstepView.swift @@ -21,7 +21,8 @@ struct AlgorithmSettingsSubstepView Date: Sat, 9 Aug 2025 14:07:54 +0200 Subject: [PATCH 02/73] chore: cleanup --- EversenseKit | 1 - 1 file changed, 1 deletion(-) delete mode 160000 EversenseKit diff --git a/EversenseKit b/EversenseKit deleted file mode 160000 index 851095c569f..00000000000 --- a/EversenseKit +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 851095c569f3673cae40358c75b10bb9ba0b6330 From bef8d7f3a1d93cf823a2e0078a7fae1758823590 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Thu, 11 Sep 2025 17:36:21 -0700 Subject: [PATCH 03/73] update MedtrumKit: 5bfb806...e695e62 --- MedtrumKit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MedtrumKit b/MedtrumKit index 5bfb80667c6..e695e62edec 160000 --- a/MedtrumKit +++ b/MedtrumKit @@ -1 +1 @@ -Subproject commit 5bfb80667c6f7f53381d07822c89dad4ee0ac60c +Subproject commit e695e62edec6fbbc43a4f51bf80d90a84e805cf1 From f98c3d16961cf8a38e458de58bd487c3e00e9cdc Mon Sep 17 00:00:00 2001 From: marionbarker Date: Thu, 11 Sep 2025 18:12:52 -0700 Subject: [PATCH 04/73] reformat swiftformat exclude line and add MedtrumKit --- scripts/swiftformat.sh | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/scripts/swiftformat.sh b/scripts/swiftformat.sh index 6780ebe96f7..981b1ae2d16 100755 --- a/scripts/swiftformat.sh +++ b/scripts/swiftformat.sh @@ -97,4 +97,19 @@ trailingClosures \ --typeattributes same-line \ --varattributes same-line \ --wrapcollections before-first \ ---exclude Pods,Generated,R.generated.swift,fastlane/swift,Dependencies, LoopKit, LibreTransmitter,G7SensorKit,OmniKit, dexcom-share-client-swift,CGMBLEKit,RileyLinkKit,OmniBLE,MinimedKit,TidepoolService +--exclude Pods, Generated, \ + R.generated.swift, \ + fastlane/swift, \ + Dependencies, \ + LoopKit, \ + LibreTransmitter, \ + G7SensorKit, \ + OmniKit, \ + dexcom-share-client-swift, \ + CGMBLEKit, \ + RileyLinkKit, \ + OmniBLE, \ + MinimedKit, \ + TidepoolService \ + DanaKit \ + MedtrumKit From 40f48e46b53a1b207beb4543be6031814e5d6f78 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Fri, 12 Sep 2025 13:29:29 -0700 Subject: [PATCH 05/73] restore app name to Trio, fix inadventent commit --- Trio/Resources/InfoPlist.xcstrings | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Trio/Resources/InfoPlist.xcstrings b/Trio/Resources/InfoPlist.xcstrings index e8388f2cc93..82e1dead5a0 100644 --- a/Trio/Resources/InfoPlist.xcstrings +++ b/Trio/Resources/InfoPlist.xcstrings @@ -8,7 +8,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "Trio Medtrum" + "value" : "Trio" } } } @@ -20,7 +20,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "Trio Medtrum" + "value" : "Trio" } } } From a26e87a87506930284390642f6ed04793d0ecc93 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Tue, 16 Sep 2025 10:23:36 -0700 Subject: [PATCH 06/73] update MedtrumKit: default alert now beep; detect pump base change, translations --- MedtrumKit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MedtrumKit b/MedtrumKit index e695e62edec..94fc976fe52 160000 --- a/MedtrumKit +++ b/MedtrumKit @@ -1 +1 @@ -Subproject commit e695e62edec6fbbc43a4f51bf80d90a84e805cf1 +Subproject commit 94fc976fe525dbaee13470f2e73e68089d373bfc From 3e5b98391943893e6da3d2e992b13c9ffc88c768 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Fri, 19 Sep 2025 07:58:28 -0700 Subject: [PATCH 07/73] update MedtrumKit: fix onboarding logic PR 24 --- MedtrumKit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MedtrumKit b/MedtrumKit index 94fc976fe52..8b46b608350 160000 --- a/MedtrumKit +++ b/MedtrumKit @@ -1 +1 @@ -Subproject commit 94fc976fe525dbaee13470f2e73e68089d373bfc +Subproject commit 8b46b60835072f02340de351d99b7c1ca2264ec1 From 35be8072da3bce5d0ff244fe841974fdfbcb5da5 Mon Sep 17 00:00:00 2001 From: bastiaanv Date: Wed, 24 Sep 2025 18:51:33 +0200 Subject: [PATCH 08/73] fix: update patch endTime after "replace patch" --- Trio/Sources/APS/DeviceDataManager.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Trio/Sources/APS/DeviceDataManager.swift b/Trio/Sources/APS/DeviceDataManager.swift index cf82b520c3f..130d7de6fa1 100644 --- a/Trio/Sources/APS/DeviceDataManager.swift +++ b/Trio/Sources/APS/DeviceDataManager.swift @@ -489,6 +489,14 @@ extension BaseDeviceDataManager: PumpManagerDelegate { storage.save(startTime, as: OpenAPS.Monitor.podAge) } } + + if let medtrumPump = pumpManager as? MedtrumPumpManager { + guard let endTime = medtrumPump.state.patchExpiresAt else { + pumpExpiresAtDate.send(nil) + return + } + pumpExpiresAtDate.send(endTime) + } if let simulatorPump = pumpManager as? MockPumpManager { broadcaster.notify(PumpReservoirObserver.self, on: processQueue) { From f905a92e26dd8942038f1878f0b735cba09986be Mon Sep 17 00:00:00 2001 From: bastiaanv Date: Sun, 5 Oct 2025 12:09:06 +0200 Subject: [PATCH 09/73] fix: keep reservoir level up-to-date on HomeScreen --- Trio/Sources/APS/DeviceDataManager.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Trio/Sources/APS/DeviceDataManager.swift b/Trio/Sources/APS/DeviceDataManager.swift index 130d7de6fa1..20b6abcab30 100644 --- a/Trio/Sources/APS/DeviceDataManager.swift +++ b/Trio/Sources/APS/DeviceDataManager.swift @@ -491,6 +491,11 @@ extension BaseDeviceDataManager: PumpManagerDelegate { } if let medtrumPump = pumpManager as? MedtrumPumpManager { + storage.save(Decimal(medtrumPump.state.reservoir), as: OpenAPS.Monitor.reservoir) + broadcaster.notify(PumpReservoirObserver.self, on: processQueue) { + $0.pumpReservoirDidChange(Decimal(medtrumPump.state.reservoir)) + } + guard let endTime = medtrumPump.state.patchExpiresAt else { pumpExpiresAtDate.send(nil) return From 8561becf59cf0694ddc63b8ec8153860e000bf1e Mon Sep 17 00:00:00 2001 From: bastiaanv Date: Sun, 12 Oct 2025 15:13:54 +0200 Subject: [PATCH 10/73] feat: show patch age if patch does not have an expiration --- MedtrumKit | 2 +- Trio/Sources/APS/APSManager.swift | 5 ++ Trio/Sources/APS/DeviceDataManager.swift | 33 ++++++++-- .../Sources/Modules/Home/HomeStateModel.swift | 6 ++ .../Modules/Home/View/Header/PumpView.swift | 65 ++++++++++++------- .../Modules/Home/View/HomeRootView.swift | 1 + 6 files changed, 80 insertions(+), 32 deletions(-) diff --git a/MedtrumKit b/MedtrumKit index 8b46b608350..fa8c1976047 160000 --- a/MedtrumKit +++ b/MedtrumKit @@ -1 +1 @@ -Subproject commit 8b46b60835072f02340de351d99b7c1ca2264ec1 +Subproject commit fa8c19760476c0aa275ddb85383f884f84aeeda9 diff --git a/Trio/Sources/APS/APSManager.swift b/Trio/Sources/APS/APSManager.swift index c00c292caeb..14eb48ad87a 100644 --- a/Trio/Sources/APS/APSManager.swift +++ b/Trio/Sources/APS/APSManager.swift @@ -18,6 +18,7 @@ protocol APSManager { var lastLoopDateSubject: PassthroughSubject { get } var bolusProgress: CurrentValueSubject { get } var pumpExpiresAtDate: CurrentValueSubject { get } + var pumpActivatedAtDate: CurrentValueSubject { get } var isManualTempBasal: Bool { get } func enactTempBasal(rate: Double, duration: TimeInterval) async func determineBasal() async throws @@ -124,6 +125,10 @@ final class BaseAPSManager: APSManager, Injectable { deviceDataManager.pumpExpiresAtDate } + var pumpActivatedAtDate: CurrentValueSubject { + deviceDataManager.pumpActivatedAtDate + } + var settings: TrioSettings { get { settingsManager.settings } set { settingsManager.settings = newValue } diff --git a/Trio/Sources/APS/DeviceDataManager.swift b/Trio/Sources/APS/DeviceDataManager.swift index 20b6abcab30..3fa7fc8f590 100644 --- a/Trio/Sources/APS/DeviceDataManager.swift +++ b/Trio/Sources/APS/DeviceDataManager.swift @@ -26,6 +26,7 @@ protocol DeviceDataManager: GlucoseSource { var errorSubject: PassthroughSubject { get } var pumpName: CurrentValueSubject { get } var pumpExpiresAtDate: CurrentValueSubject { get } + var pumpActivatedAtDate: CurrentValueSubject { get } func heartbeat(date: Date) func createBolusProgressReporter() -> DoseProgressReporter? @@ -101,6 +102,7 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable { storage.save(modifiedPreferences, as: OpenAPS.Settings.preferences) if let omnipod = pumpManager as? OmnipodPumpManager { + pumpActivatedAtDate.send(nil) guard let endTime = omnipod.state.podState?.expiresAt else { pumpExpiresAtDate.send(nil) return @@ -108,6 +110,7 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable { pumpExpiresAtDate.send(endTime) } if let omnipodBLE = pumpManager as? OmniBLEPumpManager { + pumpActivatedAtDate.send(nil) guard let endTime = omnipodBLE.state.podState?.expiresAt else { pumpExpiresAtDate.send(nil) return @@ -115,11 +118,17 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable { pumpExpiresAtDate.send(endTime) } if let medtrum = pumpManager as? MedtrumPumpManager { - guard let endTime = medtrum.state.patchExpiresAt else { + if medtrum.state.expirationTimer == 1 { + pumpActivatedAtDate.send(nil) + guard let endTime = medtrum.state.patchExpiresAt else { + pumpExpiresAtDate.send(nil) + return + } + pumpExpiresAtDate.send(endTime) + } else { + pumpActivatedAtDate.send(medtrum.state.patchActivatedAt) pumpExpiresAtDate.send(nil) - return } - pumpExpiresAtDate.send(endTime) } if let simulatorPump = pumpManager as? MockPumpManager { pumpDisplayState.value = PumpDisplayState(name: simulatorPump.localizedTitle, image: simulatorPump.smallImage) @@ -165,6 +174,7 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable { } else { pumpDisplayState.value = nil pumpExpiresAtDate.send(nil) + pumpActivatedAtDate.send(nil) pumpName.send("") // Reset bolusIncrement setting to default value, which is 0.1 U var modifiedPreferences = settingsManager.preferences @@ -204,6 +214,7 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable { let pumpDisplayState = CurrentValueSubject(nil) let pumpExpiresAtDate = CurrentValueSubject(nil) + let pumpActivatedAtDate = CurrentValueSubject(nil) let pumpName = CurrentValueSubject("Pump") init(resolver: Resolver) { @@ -446,6 +457,7 @@ extension BaseDeviceDataManager: PumpManagerDelegate { manualTempBasal.send(false) } + pumpActivatedAtDate.send(nil) guard let endTime = omnipod.state.podState?.expiresAt else { pumpExpiresAtDate.send(nil) return @@ -479,6 +491,7 @@ extension BaseDeviceDataManager: PumpManagerDelegate { manualTempBasal.send(false) } + pumpActivatedAtDate.send(nil) guard let endTime = omnipodBLE.state.podState?.expiresAt else { pumpExpiresAtDate.send(nil) return @@ -489,18 +502,24 @@ extension BaseDeviceDataManager: PumpManagerDelegate { storage.save(startTime, as: OpenAPS.Monitor.podAge) } } - + if let medtrumPump = pumpManager as? MedtrumPumpManager { storage.save(Decimal(medtrumPump.state.reservoir), as: OpenAPS.Monitor.reservoir) broadcaster.notify(PumpReservoirObserver.self, on: processQueue) { $0.pumpReservoirDidChange(Decimal(medtrumPump.state.reservoir)) } - guard let endTime = medtrumPump.state.patchExpiresAt else { + if medtrumPump.state.expirationTimer == 1 { + pumpActivatedAtDate.send(nil) + guard let endTime = medtrumPump.state.patchExpiresAt else { + pumpExpiresAtDate.send(nil) + return + } + pumpExpiresAtDate.send(endTime) + } else { + pumpActivatedAtDate.send(medtrumPump.state.patchActivatedAt) pumpExpiresAtDate.send(nil) - return } - pumpExpiresAtDate.send(endTime) } if let simulatorPump = pumpManager as? MockPumpManager { diff --git a/Trio/Sources/Modules/Home/HomeStateModel.swift b/Trio/Sources/Modules/Home/HomeStateModel.swift index 7303c88f42b..a8afb622fcb 100644 --- a/Trio/Sources/Modules/Home/HomeStateModel.swift +++ b/Trio/Sources/Modules/Home/HomeStateModel.swift @@ -48,6 +48,7 @@ extension Home { var reservoir: Decimal? var pumpName = "" var pumpExpiresAtDate: Date? + var pumpActivatedAtDate: Date? var highTTraisesSens: Bool = false var lowTTlowersSens: Bool = false var isExerciseModeActive: Bool = false @@ -343,6 +344,11 @@ extension Home { .weakAssign(to: \.pumpExpiresAtDate, on: self) .store(in: &lifetime) + apsManager.pumpActivatedAtDate + .receive(on: DispatchQueue.main) + .weakAssign(to: \.pumpActivatedAtDate, on: self) + .store(in: &lifetime) + apsManager.lastError .receive(on: DispatchQueue.main) .map { [weak self] error in diff --git a/Trio/Sources/Modules/Home/View/Header/PumpView.swift b/Trio/Sources/Modules/Home/View/Header/PumpView.swift index 0421a0bda57..5148ff2fd58 100644 --- a/Trio/Sources/Modules/Home/View/Header/PumpView.swift +++ b/Trio/Sources/Modules/Home/View/Header/PumpView.swift @@ -5,6 +5,7 @@ struct PumpView: View { let reservoir: Decimal? let name: String let expiresAtDate: Date? + let activatedAtDate: Date? let timerDate: Date let pumpStatusHighlightMessage: String? let battery: [OpenAPS_Battery] @@ -17,6 +18,7 @@ struct PumpView: View { } private var hourglassIcon: String { + if activatedAtDate != nil { return "hourglass" } guard let expiration = expiresAtDate else { return "hourglass" } let hoursRemaining = expiration.timeIntervalSince(timerDate) / 3600 @@ -96,34 +98,45 @@ struct PumpView: View { } if let date = expiresAtDate { - HStack { - Image(systemName: hourglassIcon) - .font(.callout) - .foregroundStyle(timerColor, Color.yellow) - .symbolRenderingMode(.palette) - - let remainingTimeString = remainingTimeString(time: date.timeIntervalSince(timerDate)) + PatchTimer(date, isExpiration: true) + } - Text(remainingTimeString) - .font(date.timeIntervalSince(timerDate) > 0 ? .callout : .subheadline) - .fontWeight(.bold) - .fontDesign(.rounded) - .lineLimit(2) - .multilineTextAlignment(.leading) - .frame( - // If the string is > 6 chars, i.e., exceeds "xd yh", limit width to 80 pts - // This forces the "Replace pod" string to wrap to 2 lines. - maxWidth: remainingTimeString.count > 6 ? 80 : .infinity, - alignment: .leading - ) - } - // aligns the stopwatch icon exactly with the first pixel of the reservoir icon - .padding(.leading, date.timeIntervalSince(timerDate) > 0 ? 12 : 0) + if let date = activatedAtDate { + PatchTimer(date, isExpiration: false) } } } } + @ViewBuilder private func PatchTimer(_ date: Date, isExpiration: Bool) -> some View { + HStack { + Image(systemName: hourglassIcon) + .font(.callout) + .foregroundStyle(timerColor, Color.yellow) + .symbolRenderingMode(.palette) + + let remainingTimeString = remainingTimeString( + time: isExpiration ? date.timeIntervalSince(timerDate) : timerDate + .timeIntervalSince(date) + ) + + Text(remainingTimeString) + .font(date.timeIntervalSince(timerDate) > 0 ? .callout : .subheadline) + .fontWeight(.bold) + .fontDesign(.rounded) + .lineLimit(2) + .multilineTextAlignment(.leading) + .frame( + // If the string is > 6 chars, i.e., exceeds "xd yh", limit width to 80 pts + // This forces the "Replace pod" string to wrap to 2 lines. + maxWidth: remainingTimeString.count > 6 ? 80 : .infinity, + alignment: .leading + ) + } + // aligns the stopwatch icon exactly with the first pixel of the reservoir icon + .padding(.leading, date.timeIntervalSince(timerDate) > 0 || !isExpiration ? 12 : 0) + } + private func remainingTimeString(time: TimeInterval) -> String { guard time > 0 else { return String(localized: "Replace pod", comment: "View/Header when pod expired") @@ -179,11 +192,15 @@ struct PumpView: View { } private var timerColor: Color { - guard let expisesAt = expiresAtDate else { + if activatedAtDate != nil { + return Color.loopGreen + } + + guard let expiresAt = expiresAtDate else { return .gray } - let time = expisesAt.timeIntervalSince(timerDate) + let time = expiresAt.timeIntervalSince(timerDate) switch time { case ...8.hours.timeInterval: diff --git a/Trio/Sources/Modules/Home/View/HomeRootView.swift b/Trio/Sources/Modules/Home/View/HomeRootView.swift index edc7da1725f..8f29051fa42 100644 --- a/Trio/Sources/Modules/Home/View/HomeRootView.swift +++ b/Trio/Sources/Modules/Home/View/HomeRootView.swift @@ -147,6 +147,7 @@ extension Home { reservoir: state.reservoir, name: state.pumpName, expiresAtDate: state.pumpExpiresAtDate, + activatedAtDate: state.pumpActivatedAtDate, timerDate: state.timerDate, pumpStatusHighlightMessage: state.pumpStatusHighlightMessage, battery: state.batteryFromPersistence From b6e6a5906c245f25832144deda8a4cd9bf328746 Mon Sep 17 00:00:00 2001 From: bastiaanv Date: Sun, 12 Oct 2025 15:39:28 +0200 Subject: [PATCH 11/73] chore: update Medtrum --- MedtrumKit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MedtrumKit b/MedtrumKit index fa8c1976047..14ecdef7aef 160000 --- a/MedtrumKit +++ b/MedtrumKit @@ -1 +1 @@ -Subproject commit fa8c19760476c0aa275ddb85383f884f84aeeda9 +Subproject commit 14ecdef7aef9e3a75a5bf7b2d9f34d5edc312dc0 From 862360b76c2fcf6e2f9352638d77b152843f569c Mon Sep 17 00:00:00 2001 From: bastiaanv Date: Sun, 12 Oct 2025 19:56:31 +0200 Subject: [PATCH 12/73] fix: improve time active format --- .../Modules/Home/View/Header/PumpView.swift | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/Trio/Sources/Modules/Home/View/Header/PumpView.swift b/Trio/Sources/Modules/Home/View/Header/PumpView.swift index 5148ff2fd58..c8233b5f5c9 100644 --- a/Trio/Sources/Modules/Home/View/Header/PumpView.swift +++ b/Trio/Sources/Modules/Home/View/Header/PumpView.swift @@ -115,10 +115,9 @@ struct PumpView: View { .foregroundStyle(timerColor, Color.yellow) .symbolRenderingMode(.palette) - let remainingTimeString = remainingTimeString( - time: isExpiration ? date.timeIntervalSince(timerDate) : timerDate - .timeIntervalSince(date) - ) + let remainingTimeString = isExpiration ? + remainingTimeString(time: date.timeIntervalSince(timerDate)) : + activeTimeString(time: timerDate.timeIntervalSince(date)) Text(remainingTimeString) .font(date.timeIntervalSince(timerDate) > 0 ? .callout : .subheadline) @@ -161,6 +160,23 @@ struct PumpView: View { return "\(minutes)" + String(localized: "m", comment: "abbreviation for minutes") } + private func activeTimeString(time: TimeInterval) -> String { + var time = time + let days = Int(time / 1.days.timeInterval) + time -= days.days.timeInterval + let hours = Int(time / 1.hours.timeInterval) + time -= hours.hours.timeInterval + let minutes = Int(time / 1.minutes.timeInterval) + + if days >= 1 { + return "\(days)" + String(localized: "d", comment: "abbreviation for days") + " \(hours)" + + String(localized: "h", comment: "abbreviation for hours") + } + + return "\(hours)" + String(localized: "h", comment: "abbreviation for hours") + "\(minutes)" + + String(localized: "m", comment: "abbreviation for minutes") + } + private var batteryColor: Color { guard let battery = battery.first else { return .gray From 47b119b5e77edc60d612d9f8d14085486fabe2ce Mon Sep 17 00:00:00 2001 From: bastiaanv Date: Sun, 12 Oct 2025 19:59:25 +0200 Subject: [PATCH 13/73] chore: update medtrumkit --- MedtrumKit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MedtrumKit b/MedtrumKit index 14ecdef7aef..e341df2914e 160000 --- a/MedtrumKit +++ b/MedtrumKit @@ -1 +1 @@ -Subproject commit 14ecdef7aef9e3a75a5bf7b2d9f34d5edc312dc0 +Subproject commit e341df2914e34ced52f0140c4acf1e6a4e72fdb1 From fce9722bd978163aa339de8b44974a5710b548f5 Mon Sep 17 00:00:00 2001 From: bastiaanv Date: Sat, 18 Oct 2025 15:16:36 +0200 Subject: [PATCH 14/73] chore: update hourglass icon when representing activation time --- Trio/Sources/Modules/Home/View/Header/PumpView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Trio/Sources/Modules/Home/View/Header/PumpView.swift b/Trio/Sources/Modules/Home/View/Header/PumpView.swift index c8233b5f5c9..45a4b6bdcb4 100644 --- a/Trio/Sources/Modules/Home/View/Header/PumpView.swift +++ b/Trio/Sources/Modules/Home/View/Header/PumpView.swift @@ -18,7 +18,7 @@ struct PumpView: View { } private var hourglassIcon: String { - if activatedAtDate != nil { return "hourglass" } + if activatedAtDate != nil { return "hourglass.badge.plus" } guard let expiration = expiresAtDate else { return "hourglass" } let hoursRemaining = expiration.timeIntervalSince(timerDate) / 3600 From 7dc3dc5bb61bb098e9c4e5b8653f760c37eb950d Mon Sep 17 00:00:00 2001 From: bastiaanv Date: Sat, 18 Oct 2025 15:26:28 +0200 Subject: [PATCH 15/73] chore: update MedtrumKit --- MedtrumKit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MedtrumKit b/MedtrumKit index e341df2914e..65e1d068625 160000 --- a/MedtrumKit +++ b/MedtrumKit @@ -1 +1 @@ -Subproject commit e341df2914e34ced52f0140c4acf1e6a4e72fdb1 +Subproject commit 65e1d068625dd09ffe7bd508d1d0bf37b641061d From 2da5286a873aec41b1aed1b22b86403ebfd11bf0 Mon Sep 17 00:00:00 2001 From: bastiaanv Date: Sat, 18 Oct 2025 16:29:34 +0200 Subject: [PATCH 16/73] chore: update MedtrumKit --- MedtrumKit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MedtrumKit b/MedtrumKit index 65e1d068625..f9051bed7c1 160000 --- a/MedtrumKit +++ b/MedtrumKit @@ -1 +1 @@ -Subproject commit 65e1d068625dd09ffe7bd508d1d0bf37b641061d +Subproject commit f9051bed7c1b32fe6ee50db32a46bca7fe3d6a40 From 5420be1f4cf8852d4de3303ade2bed505e04ead9 Mon Sep 17 00:00:00 2001 From: bastiaanv Date: Sun, 19 Oct 2025 09:22:17 +0200 Subject: [PATCH 17/73] wip --- Trio/Sources/Modules/Home/View/Header/PumpView.swift | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Trio/Sources/Modules/Home/View/Header/PumpView.swift b/Trio/Sources/Modules/Home/View/Header/PumpView.swift index 45a4b6bdcb4..7e357dda98d 100644 --- a/Trio/Sources/Modules/Home/View/Header/PumpView.swift +++ b/Trio/Sources/Modules/Home/View/Header/PumpView.swift @@ -112,7 +112,7 @@ struct PumpView: View { HStack { Image(systemName: hourglassIcon) .font(.callout) - .foregroundStyle(timerColor, Color.yellow) + .foregroundStyle(timerColor, timerColorSecondary) .symbolRenderingMode(.palette) let remainingTimeString = isExpiration ? @@ -227,6 +227,14 @@ struct PumpView: View { return Color.loopGreen } } + + private var timerColorSecondary: Color { + if activatedAtDate != nil { + return Color.gray + } + + return Color.yellow + } } // #Preview("message") { From 9629d763cfa568f779100288fb2aefd4d3c0904f Mon Sep 17 00:00:00 2001 From: bastiaanv Date: Sun, 19 Oct 2025 14:16:22 +0200 Subject: [PATCH 18/73] fix: update logic of + color --- Trio/Sources/Modules/Home/View/Header/PumpView.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Trio/Sources/Modules/Home/View/Header/PumpView.swift b/Trio/Sources/Modules/Home/View/Header/PumpView.swift index 7e357dda98d..e57b05f82eb 100644 --- a/Trio/Sources/Modules/Home/View/Header/PumpView.swift +++ b/Trio/Sources/Modules/Home/View/Header/PumpView.swift @@ -11,6 +11,8 @@ struct PumpView: View { let battery: [OpenAPS_Battery] @Environment(\.colorScheme) var colorScheme + let NORMAL_PATCH_AGE = TimeInterval.hours(80) + private var batteryFormatter: NumberFormatter { let formatter = NumberFormatter() formatter.numberStyle = .percent @@ -208,8 +210,8 @@ struct PumpView: View { } private var timerColor: Color { - if activatedAtDate != nil { - return Color.loopGreen + if let activatedAt = activatedAtDate { + return abs(activatedAt.timeIntervalSinceNow) > NORMAL_PATCH_AGE ? Color.yellow : Color.loopGreen } guard let expiresAt = expiresAtDate else { From bf9fd4c86222e1b22959b5be079a2973b2490673 Mon Sep 17 00:00:00 2001 From: bastiaanv Date: Mon, 20 Oct 2025 20:28:33 +0200 Subject: [PATCH 19/73] chore: update MedtrumKit --- MedtrumKit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MedtrumKit b/MedtrumKit index f9051bed7c1..62320f4ff90 160000 --- a/MedtrumKit +++ b/MedtrumKit @@ -1 +1 @@ -Subproject commit f9051bed7c1b32fe6ee50db32a46bca7fe3d6a40 +Subproject commit 62320f4ff90faf7415607df0f2dff6742adec57b From aaba29977d599a4b71b9500c71d47b5656559041 Mon Sep 17 00:00:00 2001 From: bastiaanv Date: Wed, 29 Oct 2025 17:31:50 +0100 Subject: [PATCH 20/73] chore: update MedtrumKit --- EversenseKit | 1 + MedtrumKit | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 160000 EversenseKit diff --git a/EversenseKit b/EversenseKit new file mode 160000 index 00000000000..786b0d4a1da --- /dev/null +++ b/EversenseKit @@ -0,0 +1 @@ +Subproject commit 786b0d4a1da1091a23be1aad2fe8ec41d2fe69f3 diff --git a/MedtrumKit b/MedtrumKit index 62320f4ff90..a85496e90d0 160000 --- a/MedtrumKit +++ b/MedtrumKit @@ -1 +1 @@ -Subproject commit 62320f4ff90faf7415607df0f2dff6742adec57b +Subproject commit a85496e90d0a16ba8fb5dae7781dfbc3e5f66267 From aeb6b080d6b918924d2f2afbd981928bebdc0e5a Mon Sep 17 00:00:00 2001 From: bastiaanv Date: Wed, 29 Oct 2025 17:36:00 +0100 Subject: [PATCH 21/73] chore: cleanup --- EversenseKit | 1 - 1 file changed, 1 deletion(-) delete mode 160000 EversenseKit diff --git a/EversenseKit b/EversenseKit deleted file mode 160000 index 786b0d4a1da..00000000000 --- a/EversenseKit +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 786b0d4a1da1091a23be1aad2fe8ec41d2fe69f3 From 5deff54606d08a493186226ce6cb6557c8dadd43 Mon Sep 17 00:00:00 2001 From: bastiaanv Date: Fri, 31 Oct 2025 19:56:56 +0100 Subject: [PATCH 22/73] chore: update MedtrumKit --- MedtrumKit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MedtrumKit b/MedtrumKit index a85496e90d0..4d25a76425d 160000 --- a/MedtrumKit +++ b/MedtrumKit @@ -1 +1 @@ -Subproject commit a85496e90d0a16ba8fb5dae7781dfbc3e5f66267 +Subproject commit 4d25a76425d45bebedcdaf570c495430c152abc5 From bbd5cb469785e30db26f9a0086e489086fc31176 Mon Sep 17 00:00:00 2001 From: bastiaanv Date: Thu, 6 Nov 2025 08:39:57 +0100 Subject: [PATCH 23/73] chore: update MedtrumKit --- EversenseKit | 1 + MedtrumKit | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 160000 EversenseKit diff --git a/EversenseKit b/EversenseKit new file mode 160000 index 00000000000..786b0d4a1da --- /dev/null +++ b/EversenseKit @@ -0,0 +1 @@ +Subproject commit 786b0d4a1da1091a23be1aad2fe8ec41d2fe69f3 diff --git a/MedtrumKit b/MedtrumKit index 4d25a76425d..d9bc8b5dd8f 160000 --- a/MedtrumKit +++ b/MedtrumKit @@ -1 +1 @@ -Subproject commit 4d25a76425d45bebedcdaf570c495430c152abc5 +Subproject commit d9bc8b5dd8fb304956a5624c31bc8a937ec8d4d6 From 86ceb5435deebc204368b430f0ebfde37d3b22c8 Mon Sep 17 00:00:00 2001 From: bastiaanv Date: Thu, 6 Nov 2025 08:41:17 +0100 Subject: [PATCH 24/73] chore: cleanup --- EversenseKit | 1 - 1 file changed, 1 deletion(-) delete mode 160000 EversenseKit diff --git a/EversenseKit b/EversenseKit deleted file mode 160000 index 786b0d4a1da..00000000000 --- a/EversenseKit +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 786b0d4a1da1091a23be1aad2fe8ec41d2fe69f3 From eaaf5794c65ccf65730a137845a3bc64fe91cc87 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Tue, 25 Nov 2025 11:47:49 -0800 Subject: [PATCH 25/73] update MedtrumKit: Loop specific fixes, PR 55 --- MedtrumKit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MedtrumKit b/MedtrumKit index d9bc8b5dd8f..9958befcdc8 160000 --- a/MedtrumKit +++ b/MedtrumKit @@ -1 +1 @@ -Subproject commit d9bc8b5dd8fb304956a5624c31bc8a937ec8d4d6 +Subproject commit 9958befcdc8a95b3431b80dfbfb750a3b6317d07 From 066b8b746afbf1d30d5312c98697fe2b2335045a Mon Sep 17 00:00:00 2001 From: marionbarker Date: Tue, 25 Nov 2025 12:51:35 -0800 Subject: [PATCH 26/73] update swiftformat.sh --- scripts/swiftformat.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/swiftformat.sh b/scripts/swiftformat.sh index 981b1ae2d16..3dafbc8f6fa 100755 --- a/scripts/swiftformat.sh +++ b/scripts/swiftformat.sh @@ -110,6 +110,6 @@ trailingClosures \ RileyLinkKit, \ OmniBLE, \ MinimedKit, \ - TidepoolService \ - DanaKit \ + TidepoolService, \ + DanaKit, \ MedtrumKit From 88b4edec56d4e470bda4b7cf0a30656e560139d6 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Wed, 26 Nov 2025 12:21:02 -0800 Subject: [PATCH 27/73] update DanaKit: linting change --- DanaKit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DanaKit b/DanaKit index f740e6c1858..08b6623c44b 160000 --- a/DanaKit +++ b/DanaKit @@ -1 +1 @@ -Subproject commit f740e6c1858c121dc96eb6a18b931c8fe2fb7519 +Subproject commit 08b6623c44b2ca02e39f874e46ccf800309b152c From 138284f2ee21efc0327795105a795d5a2e53451e Mon Sep 17 00:00:00 2001 From: bastiaanv Date: Sat, 20 Dec 2025 20:42:46 +0100 Subject: [PATCH 28/73] chore: update MedtrumKit --- MedtrumKit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MedtrumKit b/MedtrumKit index 9958befcdc8..aedb7bf38eb 160000 --- a/MedtrumKit +++ b/MedtrumKit @@ -1 +1 @@ -Subproject commit 9958befcdc8a95b3431b80dfbfb750a3b6317d07 +Subproject commit aedb7bf38eb6b0b7e654d8e6dcea4ae477b91c44 From 59772e033939a6b5ef13be767928c4f7c6d060df Mon Sep 17 00:00:00 2001 From: Charlie Chrisman Date: Sun, 28 Dec 2025 10:12:58 +0100 Subject: [PATCH 29/73] Implement snooze action for glucose notifications and enhance notification handling to eliminate duplicate notifications --- .../Helper/WatchNotificationHandler.swift | 101 ++++++++++++++++++ Trio Watch App Extension/TrioWatchApp.swift | 5 + .../Models/NotificationIdentifiers.swift | 26 +++++ Trio/Sources/Models/WatchMessageKeys.swift | 3 + .../Modules/Snooze/SnoozeStateModel.swift | 18 ++++ .../UserNotificationsManager.swift | 96 +++++++++++++++++ .../WatchManager/AppleWatchManager.swift | 14 ++- 7 files changed, 260 insertions(+), 3 deletions(-) create mode 100644 Trio Watch App Extension/Helper/WatchNotificationHandler.swift create mode 100644 Trio/Sources/Models/NotificationIdentifiers.swift diff --git a/Trio Watch App Extension/Helper/WatchNotificationHandler.swift b/Trio Watch App Extension/Helper/WatchNotificationHandler.swift new file mode 100644 index 00000000000..1db601119de --- /dev/null +++ b/Trio Watch App Extension/Helper/WatchNotificationHandler.swift @@ -0,0 +1,101 @@ +import Foundation +import UserNotifications +import WatchConnectivity + +final class WatchNotificationHandler: NSObject, UNUserNotificationCenterDelegate { + static let shared = WatchNotificationHandler() + + private override init() { + super.init() + } + + func configure() { + let center = UNUserNotificationCenter.current() + center.delegate = self + registerCategories(on: center) + } + + private func registerCategories(on center: UNUserNotificationCenter) { + center.getNotificationCategories { existingCategories in + let snoozeActions = NotificationResponseAction.allCases.map { action in + UNNotificationAction( + identifier: action.rawValue, + title: self.title(for: action), + options: [] + ) + } + + let glucoseCategory = UNNotificationCategory( + identifier: NotificationCategoryIdentifier.glucoseAlert.rawValue, + actions: snoozeActions, + intentIdentifiers: [], + options: [] + ) + + var categories = existingCategories + categories.update(with: glucoseCategory) + center.setNotificationCategories(categories) + } + } + + private func title(for action: NotificationResponseAction) -> String { + switch action { + case .snooze20: + return String(localized: "Snooze 20 min", comment: "Snooze glucose alerts for 20 minutes") + case .snooze40: + return String(localized: "Snooze 40 min", comment: "Snooze glucose alerts for 40 minutes") + case .snooze60: + return String(localized: "Snooze 1 hr", comment: "Snooze glucose alerts for 60 minutes") + } + } + + func userNotificationCenter( + _: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void + ) { + defer { completionHandler() } + + guard let action = NotificationResponseAction(rawValue: response.actionIdentifier) else { return } + sendSnoozeRequest(for: action) + } + + private func sendSnoozeRequest(for action: NotificationResponseAction) { + guard WCSession.isSupported() else { return } + + let payload: [String: Any] = [WatchMessageKeys.snoozeDuration: action.minutes] + let session = WCSession.default + + if session.delegate == nil { + session.delegate = PassiveSessionDelegate.shared + } + + if session.activationState == .notActivated { + session.activate() + } + + if session.isReachable { + session.sendMessage(payload, replyHandler: nil) { _ in + session.transferUserInfo(payload) + } + } else { + session.transferUserInfo(payload) + } + } +} + +private final class PassiveSessionDelegate: NSObject, WCSessionDelegate { + static let shared = PassiveSessionDelegate() + + private override init() {} + + func session( + _: WCSession, + activationDidCompleteWith _: WCSessionActivationState, + error _: Error? + ) {} + +#if os(watchOS) + func sessionReachabilityDidChange(_: WCSession) {} +#endif +} diff --git a/Trio Watch App Extension/TrioWatchApp.swift b/Trio Watch App Extension/TrioWatchApp.swift index 34eafbfca37..fe8e7f96cef 100644 --- a/Trio Watch App Extension/TrioWatchApp.swift +++ b/Trio Watch App Extension/TrioWatchApp.swift @@ -1,8 +1,13 @@ import SwiftUI +import UserNotifications @main struct TrioWatchApp: App { @Environment(\.scenePhase) private var scenePhase + init() { + WatchNotificationHandler.shared.configure() + } + var body: some Scene { WindowGroup { TrioMainWatchView() diff --git a/Trio/Sources/Models/NotificationIdentifiers.swift b/Trio/Sources/Models/NotificationIdentifiers.swift new file mode 100644 index 00000000000..098226d16b8 --- /dev/null +++ b/Trio/Sources/Models/NotificationIdentifiers.swift @@ -0,0 +1,26 @@ +import Foundation + +enum NotificationCategoryIdentifier: String { + case glucoseAlert = "Trio.glucoseAlert" +} + +enum NotificationResponseAction: String, CaseIterable { + case snooze20 = "Trio.snooze20" + case snooze40 = "Trio.snooze40" + case snooze60 = "Trio.snooze60" + + var duration: TimeInterval { + TimeInterval(minutes * 60) + } + + var minutes: Int { + switch self { + case .snooze20: + return 20 + case .snooze40: + return 40 + case .snooze60: + return 60 + } + } +} diff --git a/Trio/Sources/Models/WatchMessageKeys.swift b/Trio/Sources/Models/WatchMessageKeys.swift index b7b754bf096..68e4bdf391e 100644 --- a/Trio/Sources/Models/WatchMessageKeys.swift +++ b/Trio/Sources/Models/WatchMessageKeys.swift @@ -51,4 +51,7 @@ enum WatchMessageKeys { static let maxProtein = "maxProtein" static let bolusIncrement = "bolusIncrement" static let confirmBolusFaster = "confirmBolusFaster" + + // Notification Actions + static let snoozeDuration = "snoozeDuration" } diff --git a/Trio/Sources/Modules/Snooze/SnoozeStateModel.swift b/Trio/Sources/Modules/Snooze/SnoozeStateModel.swift index 5137d8ff7e0..23176dd709f 100644 --- a/Trio/Sources/Modules/Snooze/SnoozeStateModel.swift +++ b/Trio/Sources/Modules/Snooze/SnoozeStateModel.swift @@ -11,5 +11,23 @@ extension Snooze { override func subscribe() { alarm = glucoseStogare.alarm } + + // Add validation helper inside the class + private func validateSnoozeDuration(_ duration: TimeInterval) -> Bool { + // Only allow durations matching our defined actions + NotificationResponseAction.allCases + .map(\.duration) + .contains(duration) + } + + // Add handleSnoozeResponse inside the class + func handleSnoozeResponse(_ duration: TimeInterval) { + guard validateSnoozeDuration(duration) else { return } + + Task { @MainActor in + snoozeUntilDate = Date().addingTimeInterval(duration) + alarm = glucoseStogare.alarm + } + } } } diff --git a/Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift b/Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift index f0f70935be2..85b8fc316dc 100644 --- a/Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift +++ b/Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift @@ -11,6 +11,7 @@ import UserNotifications protocol UserNotificationsManager { func getNotificationSettings(completionHandler: @escaping (UNNotificationSettings) -> Void) func requestNotificationPermissions(completion: @escaping (Bool) -> Void) + @MainActor func applySnooze(for duration: TimeInterval) async } enum GlucoseSourceKey: String { @@ -40,6 +41,11 @@ protocol pumpNotificationObserver { func pumpRemoveNotification() } +// MARK: - SnoozeObserver Protocol +protocol SnoozeObserver { + func snoozeDidChange(_ untilDate: Date) +} + final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, Injectable { enum Identifier: String { case glucoseNotification = "Trio.glucoseNotification" @@ -61,6 +67,9 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In @Injected(as: FetchGlucoseManager.self) private var sourceInfoProvider: SourceInfoProvider! @Persisted(key: "UserNotificationsManager.snoozeUntilDate") private var snoozeUntilDate: Date = .distantPast + // The glucose notification observers below (Core Data saves and the storage publisher) can fire for the same + // reading, so we persist the last alert token to avoid enqueueing identical high/low notifications multiple times. + @Persisted(key: "UserNotificationsManager.lastGlucoseAlertToken") private var lastGlucoseAlertToken: String = "" private let notificationCenter = UNUserNotificationCenter.current() private var lifetime = Lifetime() @@ -95,11 +104,48 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In Task { await sendGlucoseNotification() } + configureNotificationCategories() registerHandlers() registerSubscribers() subscribeOnLoop() } + private func configureNotificationCategories() { + notificationCenter.getNotificationCategories { [weak self] existingCategories in + guard let self else { return } + + let snoozeActions = NotificationResponseAction.allCases.map { action in + UNNotificationAction( + identifier: action.rawValue, + title: self.title(for: action), + options: [] + ) + } + + let glucoseCategory = UNNotificationCategory( + identifier: NotificationCategoryIdentifier.glucoseAlert.rawValue, + actions: snoozeActions, + intentIdentifiers: [], + options: [] + ) + + var categories = existingCategories + categories.update(with: glucoseCategory) + self.notificationCenter.setNotificationCategories(categories) + } + } + + private func title(for action: NotificationResponseAction) -> String { + switch action { + case .snooze20: + return String(localized: "Snooze 20 min", comment: "Snooze glucose alerts for 20 minutes") + case .snooze40: + return String(localized: "Snooze 40 min", comment: "Snooze glucose alerts for 40 minutes") + case .snooze60: + return String(localized: "Snooze 1 hr", comment: "Snooze glucose alerts for 60 minutes") + } + } + private func subscribeOnLoop() { apsManager.lastLoopDateSubject .sink { [weak self] date in @@ -271,6 +317,10 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In try viewContext.existingObject(with: id) as? GlucoseStored } + if glucoseStorage.alarm == .none { + lastGlucoseAlertToken = "" + } + guard let lastReading = glucoseObjects.first?.glucose, let secondLastReading = glucoseObjects.dropFirst().first?.glucose, let lastDirection = glucoseObjects.first?.directionEnum?.symbol else { return } @@ -305,6 +355,10 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In titles.append(String(localized: "(Snoozed)", comment: "(Snoozed)")) notificationAlarm = false } else { + let token = alertToken(from: glucoseObjects.first) + if notificationAlarm, token == lastGlucoseAlertToken { + return + } titles.append(body) let content = UNMutableNotificationContent() content.title = titles.joined(separator: " ") @@ -313,6 +367,7 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In if notificationAlarm { content.sound = .default content.userInfo[NotificationAction.key] = NotificationAction.snooze.rawValue + content.categoryIdentifier = NotificationCategoryIdentifier.glucoseAlert.rawValue } addRequest( @@ -323,6 +378,9 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In messageSubtype: .glucose, action: NotificationAction.snooze ) + if notificationAlarm { + lastGlucoseAlertToken = token + } } } catch { debugPrint( @@ -331,6 +389,18 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In } } + private func alertToken(from glucose: GlucoseStored?) -> String { + if let id = glucose?.id?.uuidString { + return id + } + + if let date = glucose?.date { + return "date-\(date.timeIntervalSince1970)" + } + + return UUID().uuidString + } + private func glucoseText(glucoseValue: Int, delta: Int?, direction: String?) -> String { let units = settingsManager.settings.units let glucoseText = glucoseFormatter @@ -409,6 +479,18 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In } } + @MainActor func applySnooze(for duration: TimeInterval) async { + let untilDate = Date().addingTimeInterval(duration) + snoozeUntilDate = untilDate + lastGlucoseAlertToken = "" + removeGlucoseNotifications() + + // Notify observers that snooze was applied + broadcaster.notify(SnoozeObserver.self, on: .main) { (observer: SnoozeObserver) in + observer.snoozeDidChange(untilDate) + } + } + private func addRequest( identifier: Identifier, content: UNMutableNotificationContent, @@ -571,6 +653,12 @@ extension BaseUserNotificationsManager: pumpNotificationObserver { self.notificationCenter.removePendingNotificationRequests(withIdentifiers: [identifier.rawValue]) } } + + private func removeGlucoseNotifications() { + let identifier = Identifier.glucoseNotification.rawValue + notificationCenter.removeDeliveredNotifications(withIdentifiers: [identifier]) + notificationCenter.removePendingNotificationRequests(withIdentifiers: [identifier]) + } } extension BaseUserNotificationsManager: DeterminationObserver { @@ -601,6 +689,14 @@ extension BaseUserNotificationsManager: UNUserNotificationCenterDelegate { withCompletionHandler completionHandler: @escaping () -> Void ) { defer { completionHandler() } + + if let quickAction = NotificationResponseAction(rawValue: response.actionIdentifier) { + Task { @MainActor in + await self.applySnooze(for: quickAction.duration) + } + return + } + guard let actionRaw = response.notification.request.content.userInfo[NotificationAction.key] as? String, let action = NotificationAction(rawValue: actionRaw) else { return } diff --git a/Trio/Sources/Services/WatchManager/AppleWatchManager.swift b/Trio/Sources/Services/WatchManager/AppleWatchManager.swift index 3b8d9e47f45..8829067883c 100644 --- a/Trio/Sources/Services/WatchManager/AppleWatchManager.swift +++ b/Trio/Sources/Services/WatchManager/AppleWatchManager.swift @@ -25,6 +25,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana @Injected() private var tempTargetStorage: TempTargetsStorage! @Injected() private var bolusCalculationManager: BolusCalculationManager! @Injected() private var iobService: IOBService! + @Injected() private var notificationsManager: UserNotificationsManager! private var units: GlucoseUnits = .mgdL private var glucoseColorScheme: GlucoseColorScheme = .staticColor @@ -571,9 +572,16 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana return } - if let bolusAmount = message[WatchMessageKeys.bolus] as? Double, - message[WatchMessageKeys.carbs] == nil, - message[WatchMessageKeys.date] == nil + if let snoozeMinutes = message[WatchMessageKeys.snoozeDuration] as? Int { + debug(.watchManager, "📱 Received snooze request from watch: \(snoozeMinutes) minutes") + Task { @MainActor [weak self] in + guard let self else { return } + await self.notificationsManager.applySnooze(for: TimeInterval(snoozeMinutes * 60)) + } + return + } else if let bolusAmount = message[WatchMessageKeys.bolus] as? Double, + message[WatchMessageKeys.carbs] == nil, + message[WatchMessageKeys.date] == nil { debug(.watchManager, "📱 Received bolus request from watch: \(bolusAmount)U") self?.handleBolusRequest(Decimal(bolusAmount)) From 716c52692f0d32bca54a1e1b47e970549b6a6257 Mon Sep 17 00:00:00 2001 From: Charlie Chrisman Date: Sun, 28 Dec 2025 10:13:01 +0100 Subject: [PATCH 30/73] updated main app snooze function to use the same functions as the notification actions --- .../Modules/Snooze/SnoozeStateModel.swift | 23 +++++++++++++++++++ .../Modules/Snooze/View/SnoozeRootView.swift | 11 +++++---- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/Trio/Sources/Modules/Snooze/SnoozeStateModel.swift b/Trio/Sources/Modules/Snooze/SnoozeStateModel.swift index 23176dd709f..653c52d2cf7 100644 --- a/Trio/Sources/Modules/Snooze/SnoozeStateModel.swift +++ b/Trio/Sources/Modules/Snooze/SnoozeStateModel.swift @@ -5,11 +5,18 @@ extension Snooze { @Observable final class StateModel: BaseStateModel { @ObservationIgnored @Persisted(key: "UserNotificationsManager.snoozeUntilDate") var snoozeUntilDate: Date = .distantPast @ObservationIgnored @Injected() var glucoseStogare: GlucoseStorage! + @ObservationIgnored @Injected() var notificationsManager: UserNotificationsManager! + @ObservationIgnored @Injected() var broadcaster: Broadcaster! var alarm: GlucoseAlarm? override func subscribe() { alarm = glucoseStogare.alarm + broadcaster.register(SnoozeObserver.self, observer: self) + } + + deinit { + broadcaster.unregister(SnoozeObserver.self, observer: self) } // Add validation helper inside the class @@ -29,5 +36,21 @@ extension Snooze { alarm = glucoseStogare.alarm } } + + @MainActor func applySnooze(_ duration: TimeInterval) async { + // Allow any duration chosen in the Snooze UI, while keeping validation for quick actions elsewhere. + snoozeUntilDate = duration > 0 ? Date().addingTimeInterval(duration) : .distantPast + alarm = glucoseStogare.alarm + await notificationsManager.applySnooze(for: duration) + } + } +} + +extension Snooze.StateModel: SnoozeObserver { + func snoozeDidChange(_ untilDate: Date) { + Task { @MainActor in + snoozeUntilDate = untilDate + alarm = glucoseStogare.alarm + } } } diff --git a/Trio/Sources/Modules/Snooze/View/SnoozeRootView.swift b/Trio/Sources/Modules/Snooze/View/SnoozeRootView.swift index c4bcd603811..fa20f59b916 100644 --- a/Trio/Sources/Modules/Snooze/View/SnoozeRootView.swift +++ b/Trio/Sources/Modules/Snooze/View/SnoozeRootView.swift @@ -87,10 +87,13 @@ extension Snooze { let interval = pickerTimes[selectedInterval] let snoozeFor = formatter.string(from: interval)! let untilDate = Date() + interval - state.snoozeUntilDate = untilDate < Date() ? .distantPast : untilDate - debug(.default, "will snooze for \(snoozeFor) until \(dateFormatter.string(from: untilDate))") - snoozeDescription = getSnoozeDescription() - state.hideModal() + + Task { @MainActor in + await state.applySnooze(interval) + debug(.default, "will snooze for \(snoozeFor) until \(dateFormatter.string(from: untilDate))") + snoozeDescription = getSnoozeDescription() + state.hideModal() + } } label: { Text("Click to Snooze Alerts") .padding() From caf041d8f7ee4ddcff096ef41bf914e5bd99cf0d Mon Sep 17 00:00:00 2001 From: Charlie Chrisman Date: Sun, 28 Dec 2025 10:13:04 +0100 Subject: [PATCH 31/73] Unified in-app and notification action snooze functionality to use the same set of snooze options: 20 mins, 1 hour, 3 hours, and 6 hours, and update localized titles for all snooze actions in notification handling. --- .../Helper/WatchNotificationHandler.swift | 12 ++++---- .../Models/NotificationIdentifiers.swift | 13 +++++---- .../Modules/Snooze/View/SnoozeRootView.swift | 29 ++++--------------- .../UserNotificationsManager.swift | 12 ++++---- 4 files changed, 28 insertions(+), 38 deletions(-) diff --git a/Trio Watch App Extension/Helper/WatchNotificationHandler.swift b/Trio Watch App Extension/Helper/WatchNotificationHandler.swift index 1db601119de..2fe7df708e8 100644 --- a/Trio Watch App Extension/Helper/WatchNotificationHandler.swift +++ b/Trio Watch App Extension/Helper/WatchNotificationHandler.swift @@ -41,11 +41,13 @@ final class WatchNotificationHandler: NSObject, UNUserNotificationCenterDelegate private func title(for action: NotificationResponseAction) -> String { switch action { case .snooze20: - return String(localized: "Snooze 20 min", comment: "Snooze glucose alerts for 20 minutes") - case .snooze40: - return String(localized: "Snooze 40 min", comment: "Snooze glucose alerts for 40 minutes") - case .snooze60: - return String(localized: "Snooze 1 hr", comment: "Snooze glucose alerts for 60 minutes") + return String(localized: "20 min", comment: "Snooze glucose alerts for 20 minutes") + case .snooze1hr: + return String(localized: "1 hour", comment: "Snooze glucose alerts for 1 hour") + case .snooze3hr: + return String(localized: "3 hours", comment: "Snooze glucose alerts for 3 hours") + case .snooze6hr: + return String(localized: "6 hours", comment: "Snooze glucose alerts for 6 hours") } } diff --git a/Trio/Sources/Models/NotificationIdentifiers.swift b/Trio/Sources/Models/NotificationIdentifiers.swift index 098226d16b8..400bc0e2bcc 100644 --- a/Trio/Sources/Models/NotificationIdentifiers.swift +++ b/Trio/Sources/Models/NotificationIdentifiers.swift @@ -6,8 +6,9 @@ enum NotificationCategoryIdentifier: String { enum NotificationResponseAction: String, CaseIterable { case snooze20 = "Trio.snooze20" - case snooze40 = "Trio.snooze40" - case snooze60 = "Trio.snooze60" + case snooze1hr = "Trio.snooze1hr" + case snooze3hr = "Trio.snooze3hr" + case snooze6hr = "Trio.snooze6hr" var duration: TimeInterval { TimeInterval(minutes * 60) @@ -17,10 +18,12 @@ enum NotificationResponseAction: String, CaseIterable { switch self { case .snooze20: return 20 - case .snooze40: - return 40 - case .snooze60: + case .snooze1hr: return 60 + case .snooze3hr: + return 180 + case .snooze6hr: + return 360 } } } diff --git a/Trio/Sources/Modules/Snooze/View/SnoozeRootView.swift b/Trio/Sources/Modules/Snooze/View/SnoozeRootView.swift index fa20f59b916..080150df1e5 100644 --- a/Trio/Sources/Modules/Snooze/View/SnoozeRootView.swift +++ b/Trio/Sources/Modules/Snooze/View/SnoozeRootView.swift @@ -14,29 +14,12 @@ extension Snooze { @State private var snoozeDescription = "nothing to see here" private var pickerTimes: [TimeInterval] { - var arr: [TimeInterval] = [] - - let mins10 = 0.166_67 - let mins20 = mins10 * 2 - let mins30 = mins10 * 3 - // let mins40 = mins10 * 4 - - for hr in 0 ..< 2 { - for min in [0.0, mins20, mins20 * 2] { - arr.append(TimeInterval(hours: Double(hr) + min)) - } - } - for hr in 2 ..< 4 { - for min in [0.0, mins30] { - arr.append(TimeInterval(hours: Double(hr) + min)) - } - } - - for hr in 4 ... 8 { - arr.append(TimeInterval(hours: Double(hr))) - } - - return arr + [ + TimeInterval(minutes: 20), // 20 minutes + TimeInterval(hours: 1), // 1 hour + TimeInterval(hours: 3), // 3 hours + TimeInterval(hours: 6) // 6 hours + ] } private var formatter: DateComponentsFormatter { diff --git a/Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift b/Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift index 85b8fc316dc..2d24fa49d5e 100644 --- a/Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift +++ b/Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift @@ -138,11 +138,13 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In private func title(for action: NotificationResponseAction) -> String { switch action { case .snooze20: - return String(localized: "Snooze 20 min", comment: "Snooze glucose alerts for 20 minutes") - case .snooze40: - return String(localized: "Snooze 40 min", comment: "Snooze glucose alerts for 40 minutes") - case .snooze60: - return String(localized: "Snooze 1 hr", comment: "Snooze glucose alerts for 60 minutes") + return String(localized: "20 min", comment: "Snooze glucose alerts for 20 minutes") + case .snooze1hr: + return String(localized: "1 hour", comment: "Snooze glucose alerts for 1 hour") + case .snooze3hr: + return String(localized: "3 hours", comment: "Snooze glucose alerts for 3 hours") + case .snooze6hr: + return String(localized: "6 hours", comment: "Snooze glucose alerts for 6 hours") } } From 62b47e82ed4a12c9f8dbf7ec6523015d068e24c9 Mon Sep 17 00:00:00 2001 From: Charlie Chrisman Date: Sun, 28 Dec 2025 10:13:06 +0100 Subject: [PATCH 32/73] Refactor UI operations to ensure they execute on the main thread across various components, including snooze actions and modal handling. Updated button tap handlers and modal dismissal methods to use weak references and dispatch to the main queue as needed. --- .../Modules/Snooze/View/SnoozeRootView.swift | 13 ++++++-- .../UserNotificationsManager.swift | 30 +++++++++++-------- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/Trio/Sources/Modules/Snooze/View/SnoozeRootView.swift b/Trio/Sources/Modules/Snooze/View/SnoozeRootView.swift index 080150df1e5..ffd79fe2263 100644 --- a/Trio/Sources/Modules/Snooze/View/SnoozeRootView.swift +++ b/Trio/Sources/Modules/Snooze/View/SnoozeRootView.swift @@ -71,7 +71,8 @@ extension Snooze { let snoozeFor = formatter.string(from: interval)! let untilDate = Date() + interval - Task { @MainActor in + Task { @MainActor [weak state] in + guard let state = state else { return } await state.applySnooze(interval) debug(.default, "will snooze for \(snoozeFor) until \(dateFormatter.string(from: untilDate))") snoozeDescription = getSnoozeDescription() @@ -106,7 +107,15 @@ extension Snooze { .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme)) .navigationBarTitle("Snooze Alerts") .navigationBarTitleDisplayMode(.automatic) - .navigationBarItems(trailing: Button("Close", action: state.hideModal)) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button("Close") { + Task { @MainActor in + state.hideModal() + } + } + } + } .onAppear { configureView() snoozeDescription = getSnoozeDescription() diff --git a/Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift b/Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift index 2d24fa49d5e..a6dc0290bd9 100644 --- a/Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift +++ b/Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift @@ -703,19 +703,23 @@ extension BaseUserNotificationsManager: UNUserNotificationCenterDelegate { let action = NotificationAction(rawValue: actionRaw) else { return } - switch action { - case .snooze: - router.mainModalScreen.send(.snooze) - case .pumpConfig: - let messageCont = MessageContent( - content: response.notification.request.content.body, - type: MessageType.other, - subtype: .pump, - useAPN: false, - action: .pumpConfig - ) - router.alertMessage.send(messageCont) - default: break + // Ensure UI operations happen on main thread + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + switch action { + case .snooze: + self.router.mainModalScreen.send(.snooze) + case .pumpConfig: + let messageCont = MessageContent( + content: response.notification.request.content.body, + type: MessageType.other, + subtype: .pump, + useAPN: false, + action: .pumpConfig + ) + self.router.alertMessage.send(messageCont) + default: break + } } } } From f5053d5cb698b685f517f09d572f7a72bb060a1d Mon Sep 17 00:00:00 2001 From: Charlie Chrisman Date: Sun, 28 Dec 2025 10:13:09 +0100 Subject: [PATCH 33/73] Snooze: stabilize observer lifecycle and alert identifiers --- .../Helper/WatchNotificationHandler.swift | 2 +- Trio/Sources/Models/NotificationIdentifiers.swift | 2 +- Trio/Sources/Modules/Snooze/SnoozeStateModel.swift | 12 ++++++------ .../Sources/Modules/Snooze/View/SnoozeRootView.swift | 7 ++++--- .../UserNotifications/UserNotificationsManager.swift | 6 +++--- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/Trio Watch App Extension/Helper/WatchNotificationHandler.swift b/Trio Watch App Extension/Helper/WatchNotificationHandler.swift index 2fe7df708e8..c573818951c 100644 --- a/Trio Watch App Extension/Helper/WatchNotificationHandler.swift +++ b/Trio Watch App Extension/Helper/WatchNotificationHandler.swift @@ -26,7 +26,7 @@ final class WatchNotificationHandler: NSObject, UNUserNotificationCenterDelegate } let glucoseCategory = UNNotificationCategory( - identifier: NotificationCategoryIdentifier.glucoseAlert.rawValue, + identifier: NotificationCategoryIdentifier.trioAlert.rawValue, actions: snoozeActions, intentIdentifiers: [], options: [] diff --git a/Trio/Sources/Models/NotificationIdentifiers.swift b/Trio/Sources/Models/NotificationIdentifiers.swift index 400bc0e2bcc..a2dae546489 100644 --- a/Trio/Sources/Models/NotificationIdentifiers.swift +++ b/Trio/Sources/Models/NotificationIdentifiers.swift @@ -1,7 +1,7 @@ import Foundation enum NotificationCategoryIdentifier: String { - case glucoseAlert = "Trio.glucoseAlert" + case trioAlert = "Trio.alert" } enum NotificationResponseAction: String, CaseIterable { diff --git a/Trio/Sources/Modules/Snooze/SnoozeStateModel.swift b/Trio/Sources/Modules/Snooze/SnoozeStateModel.swift index 653c52d2cf7..ef2e393c0d0 100644 --- a/Trio/Sources/Modules/Snooze/SnoozeStateModel.swift +++ b/Trio/Sources/Modules/Snooze/SnoozeStateModel.swift @@ -4,18 +4,18 @@ import SwiftUI extension Snooze { @Observable final class StateModel: BaseStateModel { @ObservationIgnored @Persisted(key: "UserNotificationsManager.snoozeUntilDate") var snoozeUntilDate: Date = .distantPast - @ObservationIgnored @Injected() var glucoseStogare: GlucoseStorage! + @ObservationIgnored @Injected() var glucoseStorage: GlucoseStorage! @ObservationIgnored @Injected() var notificationsManager: UserNotificationsManager! @ObservationIgnored @Injected() var broadcaster: Broadcaster! var alarm: GlucoseAlarm? override func subscribe() { - alarm = glucoseStogare.alarm + alarm = glucoseStorage.alarm broadcaster.register(SnoozeObserver.self, observer: self) } - deinit { + func unsubscribe() { broadcaster.unregister(SnoozeObserver.self, observer: self) } @@ -33,14 +33,14 @@ extension Snooze { Task { @MainActor in snoozeUntilDate = Date().addingTimeInterval(duration) - alarm = glucoseStogare.alarm + alarm = glucoseStorage.alarm } } @MainActor func applySnooze(_ duration: TimeInterval) async { // Allow any duration chosen in the Snooze UI, while keeping validation for quick actions elsewhere. snoozeUntilDate = duration > 0 ? Date().addingTimeInterval(duration) : .distantPast - alarm = glucoseStogare.alarm + alarm = glucoseStorage.alarm await notificationsManager.applySnooze(for: duration) } } @@ -50,7 +50,7 @@ extension Snooze.StateModel: SnoozeObserver { func snoozeDidChange(_ untilDate: Date) { Task { @MainActor in snoozeUntilDate = untilDate - alarm = glucoseStogare.alarm + alarm = glucoseStorage.alarm } } } diff --git a/Trio/Sources/Modules/Snooze/View/SnoozeRootView.swift b/Trio/Sources/Modules/Snooze/View/SnoozeRootView.swift index ffd79fe2263..180ccccf1ea 100644 --- a/Trio/Sources/Modules/Snooze/View/SnoozeRootView.swift +++ b/Trio/Sources/Modules/Snooze/View/SnoozeRootView.swift @@ -110,9 +110,7 @@ extension Snooze { .toolbar { ToolbarItem(placement: .topBarTrailing) { Button("Close") { - Task { @MainActor in - state.hideModal() - } + state.hideModal() } } } @@ -120,6 +118,9 @@ extension Snooze { configureView() snoozeDescription = getSnoozeDescription() } + .onDisappear { + state.unsubscribe() + } } } } diff --git a/Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift b/Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift index a6dc0290bd9..f3affcbe21d 100644 --- a/Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift +++ b/Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift @@ -43,7 +43,7 @@ protocol pumpNotificationObserver { // MARK: - SnoozeObserver Protocol protocol SnoozeObserver { - func snoozeDidChange(_ untilDate: Date) + @MainActor func snoozeDidChange(_ untilDate: Date) } final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, Injectable { @@ -123,7 +123,7 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In } let glucoseCategory = UNNotificationCategory( - identifier: NotificationCategoryIdentifier.glucoseAlert.rawValue, + identifier: NotificationCategoryIdentifier.trioAlert.rawValue, actions: snoozeActions, intentIdentifiers: [], options: [] @@ -369,7 +369,7 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In if notificationAlarm { content.sound = .default content.userInfo[NotificationAction.key] = NotificationAction.snooze.rawValue - content.categoryIdentifier = NotificationCategoryIdentifier.glucoseAlert.rawValue + content.categoryIdentifier = NotificationCategoryIdentifier.trioAlert.rawValue } addRequest( From c9dd44c29bb1e13e2dfa45f19b4916ee855a012b Mon Sep 17 00:00:00 2001 From: Charlie Chrisman Date: Sun, 28 Dec 2025 10:13:11 +0100 Subject: [PATCH 34/73] Refactor notification handling to use NotificationCategoryFactory --- .../Helper/WatchNotificationHandler.swift | 73 +++---------------- .../Models/NotificationIdentifiers.swift | 37 +++++++++- .../UserNotificationsManager.swift | 68 ++++++++--------- 3 files changed, 79 insertions(+), 99 deletions(-) diff --git a/Trio Watch App Extension/Helper/WatchNotificationHandler.swift b/Trio Watch App Extension/Helper/WatchNotificationHandler.swift index c573818951c..3a0815c7f99 100644 --- a/Trio Watch App Extension/Helper/WatchNotificationHandler.swift +++ b/Trio Watch App Extension/Helper/WatchNotificationHandler.swift @@ -5,7 +5,7 @@ import WatchConnectivity final class WatchNotificationHandler: NSObject, UNUserNotificationCenterDelegate { static let shared = WatchNotificationHandler() - private override init() { + override private init() { super.init() } @@ -17,40 +17,19 @@ final class WatchNotificationHandler: NSObject, UNUserNotificationCenterDelegate private func registerCategories(on center: UNUserNotificationCenter) { center.getNotificationCategories { existingCategories in - let snoozeActions = NotificationResponseAction.allCases.map { action in - UNNotificationAction( - identifier: action.rawValue, - title: self.title(for: action), - options: [] - ) - } - - let glucoseCategory = UNNotificationCategory( - identifier: NotificationCategoryIdentifier.trioAlert.rawValue, - actions: snoozeActions, - intentIdentifiers: [], - options: [] - ) + let glucoseCategory = NotificationCategoryFactory.createGlucoseCategory() var categories = existingCategories categories.update(with: glucoseCategory) - center.setNotificationCategories(categories) - } - } - - private func title(for action: NotificationResponseAction) -> String { - switch action { - case .snooze20: - return String(localized: "20 min", comment: "Snooze glucose alerts for 20 minutes") - case .snooze1hr: - return String(localized: "1 hour", comment: "Snooze glucose alerts for 1 hour") - case .snooze3hr: - return String(localized: "3 hours", comment: "Snooze glucose alerts for 3 hours") - case .snooze6hr: - return String(localized: "6 hours", comment: "Snooze glucose alerts for 6 hours") + // UNUserNotificationCenter methods should be called on main thread + Task { @MainActor in + center.setNotificationCategories(categories) + } } } + /// UNUserNotificationCenterDelegate method called when user interacts with a notification on watch. + /// This can be called off the main thread. WCSession.transferUserInfo is thread-safe. func userNotificationCenter( _: UNUserNotificationCenter, didReceive response: UNNotificationResponse, @@ -62,42 +41,12 @@ final class WatchNotificationHandler: NSObject, UNUserNotificationCenterDelegate sendSnoozeRequest(for: action) } + /// Sends snooze request to iPhone via WatchConnectivity. + /// WCSession.transferUserInfo is thread-safe and can be called from any thread. private func sendSnoozeRequest(for action: NotificationResponseAction) { guard WCSession.isSupported() else { return } let payload: [String: Any] = [WatchMessageKeys.snoozeDuration: action.minutes] - let session = WCSession.default - - if session.delegate == nil { - session.delegate = PassiveSessionDelegate.shared - } - - if session.activationState == .notActivated { - session.activate() - } - - if session.isReachable { - session.sendMessage(payload, replyHandler: nil) { _ in - session.transferUserInfo(payload) - } - } else { - session.transferUserInfo(payload) - } + WCSession.default.transferUserInfo(payload) } } - -private final class PassiveSessionDelegate: NSObject, WCSessionDelegate { - static let shared = PassiveSessionDelegate() - - private override init() {} - - func session( - _: WCSession, - activationDidCompleteWith _: WCSessionActivationState, - error _: Error? - ) {} - -#if os(watchOS) - func sessionReachabilityDidChange(_: WCSession) {} -#endif -} diff --git a/Trio/Sources/Models/NotificationIdentifiers.swift b/Trio/Sources/Models/NotificationIdentifiers.swift index a2dae546489..335c28096b3 100644 --- a/Trio/Sources/Models/NotificationIdentifiers.swift +++ b/Trio/Sources/Models/NotificationIdentifiers.swift @@ -1,4 +1,5 @@ import Foundation +import UserNotifications enum NotificationCategoryIdentifier: String { case trioAlert = "Trio.alert" @@ -11,7 +12,7 @@ enum NotificationResponseAction: String, CaseIterable { case snooze6hr = "Trio.snooze6hr" var duration: TimeInterval { - TimeInterval(minutes * 60) + TimeInterval(minutes) * 60 } var minutes: Int { @@ -26,4 +27,38 @@ enum NotificationResponseAction: String, CaseIterable { return 360 } } + + var localizedTitle: String { + switch self { + case .snooze20: + return String(localized: "20 min", comment: "Snooze glucose alerts for 20 minutes") + case .snooze1hr: + return String(localized: "1 hour", comment: "Snooze glucose alerts for 1 hour") + case .snooze3hr: + return String(localized: "3 hours", comment: "Snooze glucose alerts for 3 hours") + case .snooze6hr: + return String(localized: "6 hours", comment: "Snooze glucose alerts for 6 hours") + } + } +} + +// MARK: - NotificationCategoryFactory + +enum NotificationCategoryFactory { + static func createGlucoseCategory() -> UNNotificationCategory { + let snoozeActions = NotificationResponseAction.allCases.map { action in + UNNotificationAction( + identifier: action.rawValue, + title: action.localizedTitle, + options: [] + ) + } + + return UNNotificationCategory( + identifier: NotificationCategoryIdentifier.trioAlert.rawValue, + actions: snoozeActions, + intentIdentifiers: [], + options: [] + ) + } } diff --git a/Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift b/Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift index f3affcbe21d..f595157a138 100644 --- a/Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift +++ b/Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift @@ -42,6 +42,7 @@ protocol pumpNotificationObserver { } // MARK: - SnoozeObserver Protocol + protocol SnoozeObserver { @MainActor func snoozeDidChange(_ untilDate: Date) } @@ -114,37 +115,15 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In notificationCenter.getNotificationCategories { [weak self] existingCategories in guard let self else { return } - let snoozeActions = NotificationResponseAction.allCases.map { action in - UNNotificationAction( - identifier: action.rawValue, - title: self.title(for: action), - options: [] - ) - } - - let glucoseCategory = UNNotificationCategory( - identifier: NotificationCategoryIdentifier.trioAlert.rawValue, - actions: snoozeActions, - intentIdentifiers: [], - options: [] - ) + let glucoseCategory = NotificationCategoryFactory.createGlucoseCategory() var categories = existingCategories categories.update(with: glucoseCategory) - self.notificationCenter.setNotificationCategories(categories) - } - } - - private func title(for action: NotificationResponseAction) -> String { - switch action { - case .snooze20: - return String(localized: "20 min", comment: "Snooze glucose alerts for 20 minutes") - case .snooze1hr: - return String(localized: "1 hour", comment: "Snooze glucose alerts for 1 hour") - case .snooze3hr: - return String(localized: "3 hours", comment: "Snooze glucose alerts for 3 hours") - case .snooze6hr: - return String(localized: "6 hours", comment: "Snooze glucose alerts for 6 hours") + // UNUserNotificationCenter methods should be called on main thread + Task { @MainActor [weak self] in + guard let self else { return } + self.notificationCenter.setNotificationCategories(categories) + } } } @@ -358,6 +337,11 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In notificationAlarm = false } else { let token = alertToken(from: glucoseObjects.first) + + if token == "unknown" { + warning(.service, "Missing glucose token fields; skipping notification to avoid re-alerting") + return + } if notificationAlarm, token == lastGlucoseAlertToken { return } @@ -392,15 +376,20 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In } private func alertToken(from glucose: GlucoseStored?) -> String { - if let id = glucose?.id?.uuidString { - return id - } + if let id = glucose?.id?.uuidString { return id } if let date = glucose?.date { - return "date-\(date.timeIntervalSince1970)" + let roundedMinute = Int((date.timeIntervalSince1970 / 60).rounded()) + return "date-\(roundedMinute)" + } + + // Stable fallback for Core Data objects: + if let glucose, !glucose.objectID.isTemporaryID { + return "objectID-\(glucose.objectID.uriRepresentation().absoluteString)" } - return UUID().uuidString + // Stable “unknown” fallback: prevents repeated alarms when identifiers are missing + return "unknown" } private func glucoseText(glucoseValue: Int, delta: Int?, direction: String?) -> String { @@ -485,6 +474,7 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In let untilDate = Date().addingTimeInterval(duration) snoozeUntilDate = untilDate lastGlucoseAlertToken = "" + // removeGlucoseNotifications() is safe to call here since we're @MainActor removeGlucoseNotifications() // Notify observers that snooze was applied @@ -656,7 +646,9 @@ extension BaseUserNotificationsManager: pumpNotificationObserver { } } - private func removeGlucoseNotifications() { + /// Removes all glucose notifications (delivered and pending). + /// Must be called from the main thread. Safe to call from @MainActor contexts. + @MainActor private func removeGlucoseNotifications() { let identifier = Identifier.glucoseNotification.rawValue notificationCenter.removeDeliveredNotifications(withIdentifiers: [identifier]) notificationCenter.removePendingNotificationRequests(withIdentifiers: [identifier]) @@ -685,6 +677,8 @@ extension BaseUserNotificationsManager: UNUserNotificationCenterDelegate { completionHandler([.banner, .badge, .sound, .list]) } + /// UNUserNotificationCenterDelegate method called when user interacts with a notification. + /// This can be called off the main thread, so we ensure all work happens on @MainActor. func userNotificationCenter( _: UNUserNotificationCenter, didReceive response: UNNotificationResponse, @@ -692,6 +686,7 @@ extension BaseUserNotificationsManager: UNUserNotificationCenterDelegate { ) { defer { completionHandler() } + // Handle quick snooze actions (from notification action buttons) if let quickAction = NotificationResponseAction(rawValue: response.actionIdentifier) { Task { @MainActor in await self.applySnooze(for: quickAction.duration) @@ -699,12 +694,13 @@ extension BaseUserNotificationsManager: UNUserNotificationCenterDelegate { return } + // Handle other notification actions (e.g., tapping notification body) guard let actionRaw = response.notification.request.content.userInfo[NotificationAction.key] as? String, let action = NotificationAction(rawValue: actionRaw) else { return } - // Ensure UI operations happen on main thread - DispatchQueue.main.async { [weak self] in + // Ensure UI operations happen on main thread using Task for consistency + Task { @MainActor [weak self] in guard let self = self else { return } switch action { case .snooze: From 904bb573cf9d5269643eb3c56227b56c4f0aed51 Mon Sep 17 00:00:00 2001 From: Charlie Chrisman Date: Thu, 8 Jan 2026 14:23:45 +0100 Subject: [PATCH 35/73] fix(notifications): standardize threading and fix/improve watch connectivity for snooze - Standardize all snooze-related async work to Task { @MainActor [weak self] } instead of mixing DispatchQueue.main.async and Task patterns - Fix/improve watch connectivity reliability: watch was sending snooze via transferUserInfo, but phone was expecting snooze in didReceiveMessage. Updated so watch uses sendMessage with transferUserInfo fallback for immediate delivery when reachable, queued when not and phone is ready to receive in both didReceiveMessage and didReceiveUserInfo. - Enhance alert deduplication beyond original patch: round timestamps to minutes and add stable Core Data objectID fallback to prevent duplicate notifications --- .../Helper/WatchNotificationHandler.swift | 16 +++++++- .../UserNotificationsManager.swift | 3 +- .../WatchManager/AppleWatchManager.swift | 41 +++++++++++-------- 3 files changed, 41 insertions(+), 19 deletions(-) diff --git a/Trio Watch App Extension/Helper/WatchNotificationHandler.swift b/Trio Watch App Extension/Helper/WatchNotificationHandler.swift index 3a0815c7f99..112431e2acb 100644 --- a/Trio Watch App Extension/Helper/WatchNotificationHandler.swift +++ b/Trio Watch App Extension/Helper/WatchNotificationHandler.swift @@ -43,10 +43,24 @@ final class WatchNotificationHandler: NSObject, UNUserNotificationCenterDelegate /// Sends snooze request to iPhone via WatchConnectivity. /// WCSession.transferUserInfo is thread-safe and can be called from any thread. + /// Relies on the watch app's WCSession owner (e.g., WatchState) to handle + /// session activation and delegate management. private func sendSnoozeRequest(for action: NotificationResponseAction) { guard WCSession.isSupported() else { return } let payload: [String: Any] = [WatchMessageKeys.snoozeDuration: action.minutes] - WCSession.default.transferUserInfo(payload) + let session = WCSession.default + + // Try sendMessage first if session is reachable and activated (faster, immediate delivery) + // Fall back to transferUserInfo if not reachable or if sendMessage fails + if session.isReachable && session.activationState == .activated { + session.sendMessage(payload, replyHandler: nil) { _ in + // Fallback to transferUserInfo if sendMessage fails + session.transferUserInfo(payload) + } + } else { + // Session not reachable or not activated - use transferUserInfo (queued delivery) + session.transferUserInfo(payload) + } } } diff --git a/Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift b/Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift index f595157a138..6e14f3630c6 100644 --- a/Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift +++ b/Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift @@ -688,7 +688,8 @@ extension BaseUserNotificationsManager: UNUserNotificationCenterDelegate { // Handle quick snooze actions (from notification action buttons) if let quickAction = NotificationResponseAction(rawValue: response.actionIdentifier) { - Task { @MainActor in + Task { @MainActor [weak self] in + guard let self else { return } await self.applySnooze(for: quickAction.duration) } return diff --git a/Trio/Sources/Services/WatchManager/AppleWatchManager.swift b/Trio/Sources/Services/WatchManager/AppleWatchManager.swift index 8829067883c..4d24ca98e1d 100644 --- a/Trio/Sources/Services/WatchManager/AppleWatchManager.swift +++ b/Trio/Sources/Services/WatchManager/AppleWatchManager.swift @@ -552,16 +552,18 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana } func session(_: WCSession, didReceiveMessage message: [String: Any]) { - DispatchQueue.main.async { [weak self] in - if let logs = message["watchLogs"] as? String { - SimpleLogReporter.appendToWatchLog(logs) - } + // Handle logs first - doesn't need self, so it can run even during teardown + if let logs = message["watchLogs"] as? String { + SimpleLogReporter.appendToWatchLog(logs) + } + + Task { @MainActor [weak self] in + guard let self else { return } if let requestWatchUpdate = message[WatchMessageKeys.requestWatchUpdate] as? String, requestWatchUpdate == WatchMessageKeys.watchState { debug(.watchManager, "📱 Watch requested watch state data update.") - guard let self = self else { return } // Skip if no watch is paired or app not installed guard let session = self.session, session.isPaired, session.isReachable, session.isWatchAppInstalled else { return } @@ -574,24 +576,21 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana if let snoozeMinutes = message[WatchMessageKeys.snoozeDuration] as? Int { debug(.watchManager, "📱 Received snooze request from watch: \(snoozeMinutes) minutes") - Task { @MainActor [weak self] in - guard let self else { return } - await self.notificationsManager.applySnooze(for: TimeInterval(snoozeMinutes * 60)) - } + await self.notificationsManager.applySnooze(for: TimeInterval(snoozeMinutes * 60)) return } else if let bolusAmount = message[WatchMessageKeys.bolus] as? Double, message[WatchMessageKeys.carbs] == nil, message[WatchMessageKeys.date] == nil { debug(.watchManager, "📱 Received bolus request from watch: \(bolusAmount)U") - self?.handleBolusRequest(Decimal(bolusAmount)) + self.handleBolusRequest(Decimal(bolusAmount)) } else if let carbsAmount = message[WatchMessageKeys.carbs] as? Int, let timestamp = message[WatchMessageKeys.date] as? TimeInterval, message[WatchMessageKeys.bolus] == nil { let date = Date(timeIntervalSince1970: timestamp) debug(.watchManager, "📱 Received carbs request from watch: \(carbsAmount)g at \(date)") - self?.handleCarbsRequest(carbsAmount, date) + self.handleCarbsRequest(carbsAmount, date) } else if let bolusAmount = message[WatchMessageKeys.bolus] as? Double, let carbsAmount = message[WatchMessageKeys.carbs] as? Int, let timestamp = message[WatchMessageKeys.date] as? TimeInterval @@ -601,11 +600,11 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana .watchManager, "📱 Received meal bolus combo request from watch: \(bolusAmount)U, \(carbsAmount)g at \(date)" ) - self?.handleCombinedRequest(bolusAmount: Decimal(bolusAmount), carbsAmount: Decimal(carbsAmount), date: date) + self.handleCombinedRequest(bolusAmount: Decimal(bolusAmount), carbsAmount: Decimal(carbsAmount), date: date) } else { debug(.watchManager, "📱 Invalid or incomplete data received from watch. Received: \(message)") // Acknowledge failure - self?.sendAcknowledgment( + self.sendAcknowledgment( toWatch: false, message: "Error! Invalid or incomplete data received from watch.", ackCode: .genericFailure @@ -614,22 +613,22 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana if message[WatchMessageKeys.cancelOverride] as? Bool == true { debug(.watchManager, "📱 Received cancel override request from watch") - self?.handleCancelOverride() + self.handleCancelOverride() } if let presetName = message[WatchMessageKeys.activateOverride] as? String { debug(.watchManager, "📱 Received activate override request from watch for preset: \(presetName)") - self?.handleActivateOverride(presetName) + self.handleActivateOverride(presetName) } if let presetName = message[WatchMessageKeys.activateTempTarget] as? String { debug(.watchManager, "📱 Received activate temp target request from watch for preset: \(presetName)") - self?.handleActivateTempTarget(presetName) + self.handleActivateTempTarget(presetName) } if message[WatchMessageKeys.cancelTempTarget] as? Bool == true { debug(.watchManager, "📱 Received cancel temp target request from watch") - self?.handleCancelTempTarget() + self.handleCancelTempTarget() } if message[WatchMessageKeys.requestBolusRecommendation] as? Bool == true { @@ -690,6 +689,14 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana if let logs = userInfo["watchLogs"] as? String { SimpleLogReporter.appendToWatchLog(logs) } + + if let snoozeMinutes = userInfo[WatchMessageKeys.snoozeDuration] as? Int { + debug(.watchManager, "📱 Received snooze userInfo from watch: \(snoozeMinutes) minutes") + Task { @MainActor [weak self] in + guard let self else { return } + await self.notificationsManager.applySnooze(for: TimeInterval(snoozeMinutes * 60)) + } + } } #if os(iOS) From c17bb518d096da65639240dff0fd5af511195b1f Mon Sep 17 00:00:00 2001 From: marionbarker Date: Mon, 12 Jan 2026 11:02:40 -0800 Subject: [PATCH 36/73] update MedtrumKit: aedb7bf...0905638; * fix: prevent hanging if reconnect failed; * fix: better rounding bolus Entries, wasreporting interrupted bolus when bolus had completed; * * localizations --- MedtrumKit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MedtrumKit b/MedtrumKit index aedb7bf38eb..0905638ed04 160000 --- a/MedtrumKit +++ b/MedtrumKit @@ -1 +1 @@ -Subproject commit aedb7bf38eb6b0b7e654d8e6dcea4ae477b91c44 +Subproject commit 0905638ed0430eca5f36ad3834a1e229db2ad6fe From 119e5da7ccb6cff5d096efb134f2c0fe44319bbf Mon Sep 17 00:00:00 2001 From: bastiaanv Date: Wed, 14 Jan 2026 21:25:09 +0100 Subject: [PATCH 37/73] chore: update MedtrumKit --- EversenseKit | 1 + MedtrumKit | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 160000 EversenseKit diff --git a/EversenseKit b/EversenseKit new file mode 160000 index 00000000000..2204e6267a9 --- /dev/null +++ b/EversenseKit @@ -0,0 +1 @@ +Subproject commit 2204e6267a9140e3491c6c4dad1875951cbfb275 diff --git a/MedtrumKit b/MedtrumKit index 0905638ed04..1a3b75ea013 160000 --- a/MedtrumKit +++ b/MedtrumKit @@ -1 +1 @@ -Subproject commit 0905638ed0430eca5f36ad3834a1e229db2ad6fe +Subproject commit 1a3b75ea01303f299db335076302b0bf9d1d9f21 From 0a69738d3704a45c397bdd4244a6283bd75c3ca5 Mon Sep 17 00:00:00 2001 From: bastiaanv Date: Wed, 14 Jan 2026 21:26:05 +0100 Subject: [PATCH 38/73] chore: cleanup --- EversenseKit | 1 - 1 file changed, 1 deletion(-) delete mode 160000 EversenseKit diff --git a/EversenseKit b/EversenseKit deleted file mode 160000 index 2204e6267a9..00000000000 --- a/EversenseKit +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2204e6267a9140e3491c6c4dad1875951cbfb275 From eee3468671e6ac70fc61414c599dc74acecb3d43 Mon Sep 17 00:00:00 2001 From: Deniz Cengiz Date: Mon, 26 Jan 2026 19:27:06 +0100 Subject: [PATCH 39/73] Sync with upstream MedtrumKit (loopandlearn/MedtrumKit#f21d808) --- MedtrumKit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MedtrumKit b/MedtrumKit index 1a3b75ea013..f21d80863d2 160000 --- a/MedtrumKit +++ b/MedtrumKit @@ -1 +1 @@ -Subproject commit 1a3b75ea01303f299db335076302b0bf9d1d9f21 +Subproject commit f21d80863d290c1cae9d33d1b40ac836e754f8a4 From c481012b9c02c88f32be38df7b8bef673cf60022 Mon Sep 17 00:00:00 2001 From: Deniz Cengiz Date: Tue, 24 Mar 2026 00:59:14 +0100 Subject: [PATCH 40/73] Bump MedtrumKit --- MedtrumKit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MedtrumKit b/MedtrumKit index f21d80863d2..43ef0424a85 160000 --- a/MedtrumKit +++ b/MedtrumKit @@ -1 +1 @@ -Subproject commit f21d80863d290c1cae9d33d1b40ac836e754f8a4 +Subproject commit 43ef0424a8562aae0cf8995511b3b5d559240c16 From ec479feb5389fe21ff4a45e8d1bffcfe3a4b0962 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Tue, 24 Mar 2026 17:56:42 +0100 Subject: [PATCH 41/73] Update actions to Node.js 24 compatible versions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - actions/checkout v4 → v5 - actions/upload-artifact v4 → v6 - actions/cache v4 → v5 - actions/stale v9 → v10 - Fork-Sync-With-Upstream-action v3.4.1 → v3.4.2 --- .github/workflows/add_identifiers.yml | 2 +- .github/workflows/auto_version_dev.yml | 2 +- .github/workflows/build_trio.yml | 8 ++++---- .github/workflows/create_certs.yml | 4 ++-- .github/workflows/stale_issues.yml | 4 ++-- .github/workflows/unit_tests.yml | 6 +++--- .github/workflows/validate_secrets.yml | 2 +- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/add_identifiers.yml b/.github/workflows/add_identifiers.yml index 3a1bfc70992..2fe6142984e 100644 --- a/.github/workflows/add_identifiers.yml +++ b/.github/workflows/add_identifiers.yml @@ -16,7 +16,7 @@ jobs: steps: # Checks-out the repo - name: Checkout Repo - uses: actions/checkout@v4 + uses: actions/checkout@v5 # Patch Fastlane Match to not print tables - name: Patch Match Tables diff --git a/.github/workflows/auto_version_dev.yml b/.github/workflows/auto_version_dev.yml index f94c93fe154..0f5536036e1 100644 --- a/.github/workflows/auto_version_dev.yml +++ b/.github/workflows/auto_version_dev.yml @@ -52,7 +52,7 @@ jobs: steps: - name: Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: token: ${{ secrets.TRIO_TOKEN_AUTOBUMP }} diff --git a/.github/workflows/build_trio.yml b/.github/workflows/build_trio.yml index c8a752403a9..b48d50b2c11 100644 --- a/.github/workflows/build_trio.yml +++ b/.github/workflows/build_trio.yml @@ -90,7 +90,7 @@ jobs: if: | steps.workflow-permission.outputs.has_permission == 'true' && (vars.SCHEDULED_BUILD != 'false' || vars.SCHEDULED_SYNC != 'false') - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: token: ${{ secrets.GH_PAT }} @@ -100,7 +100,7 @@ jobs: steps.workflow-permission.outputs.has_permission == 'true' && vars.SCHEDULED_SYNC != 'false' && github.repository_owner != 'nightscout' id: sync - uses: aormsby/Fork-Sync-With-Upstream-action@v3.4.1 + uses: aormsby/Fork-Sync-With-Upstream-action@v3.4.2 with: target_sync_branch: ${{ env.TARGET_BRANCH }} shallow_since: 6 months ago @@ -178,7 +178,7 @@ jobs: run: "sudo xcode-select --switch /Applications/Xcode_26.2.app/Contents/Developer" - name: Checkout Repo for building - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: token: ${{ secrets.GH_PAT }} submodules: recursive @@ -258,7 +258,7 @@ jobs: # Upload Build artifacts - name: Upload build log, IPA and Symbol artifacts if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: build-artifacts path: | diff --git a/.github/workflows/create_certs.yml b/.github/workflows/create_certs.yml index 430fbe1fdab..e8f998afb1f 100644 --- a/.github/workflows/create_certs.yml +++ b/.github/workflows/create_certs.yml @@ -28,7 +28,7 @@ jobs: steps: # Checks-out the repo - name: Checkout Repo - uses: actions/checkout@v4 + uses: actions/checkout@v5 # Patch Fastlane Match to not print tables - name: Patch Match Tables @@ -97,7 +97,7 @@ jobs: run: echo "new_certificate_needed=${{ needs.create_certs.outputs.new_certificate_needed }}" - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Install dependencies run: bundle install diff --git a/.github/workflows/stale_issues.yml b/.github/workflows/stale_issues.yml index 7252afefd46..4f7e3e1ce41 100644 --- a/.github/workflows/stale_issues.yml +++ b/.github/workflows/stale_issues.yml @@ -11,7 +11,7 @@ jobs: pull-requests: write if: github.repository_owner == 'nightscout' steps: - - uses: actions/stale@v9.0.0 + - uses: actions/stale@v10 with: days-before-issue-stale: 30 days-before-issue-close: 14 @@ -32,7 +32,7 @@ jobs: pull-requests: write if: github.repository_owner == 'nightscout' steps: - - uses: actions/stale@v9.0.0 + - uses: actions/stale@v10 with: days-before-issue-stale: 30 days-before-issue-close: 30 diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 2032514d96d..255f880cc87 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -31,14 +31,14 @@ jobs: run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 1 submodules: recursive - name: Restore cache id: cache-restore - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: | /Users/runner/Library/Developer/Xcode/DerivedData @@ -94,7 +94,7 @@ jobs: - name: Save cache if: steps.cache-restore.outputs.cache-hit != 'true' - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 with: path: | /Users/runner/Library/Developer/Xcode/DerivedData diff --git a/.github/workflows/validate_secrets.yml b/.github/workflows/validate_secrets.yml index d30842026f6..ea150c0c53d 100644 --- a/.github/workflows/validate_secrets.yml +++ b/.github/workflows/validate_secrets.yml @@ -116,7 +116,7 @@ jobs: TEAMID: ${{ secrets.TEAMID }} steps: - name: Checkout Repo - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Install Project Dependencies run: bundle install From ebab072e5ba5305ffbf816d198f0d5d9e8760ec8 Mon Sep 17 00:00:00 2001 From: Trio Build Bot Date: Wed, 25 Mar 2026 14:32:52 +0100 Subject: [PATCH 42/73] Refactor Snooze functionality by removing the handleSnoozeResponse method and updating the formatInterval method to handle nil values gracefully. Adjusted snooze description logic to utilize the new formatInterval method. --- Trio/Sources/Modules/Snooze/SnoozeStateModel.swift | 10 ---------- Trio/Sources/Modules/Snooze/View/SnoozeRootView.swift | 4 ++-- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/Trio/Sources/Modules/Snooze/SnoozeStateModel.swift b/Trio/Sources/Modules/Snooze/SnoozeStateModel.swift index ef2e393c0d0..1ff5beba6fb 100644 --- a/Trio/Sources/Modules/Snooze/SnoozeStateModel.swift +++ b/Trio/Sources/Modules/Snooze/SnoozeStateModel.swift @@ -27,16 +27,6 @@ extension Snooze { .contains(duration) } - // Add handleSnoozeResponse inside the class - func handleSnoozeResponse(_ duration: TimeInterval) { - guard validateSnoozeDuration(duration) else { return } - - Task { @MainActor in - snoozeUntilDate = Date().addingTimeInterval(duration) - alarm = glucoseStorage.alarm - } - } - @MainActor func applySnooze(_ duration: TimeInterval) async { // Allow any duration chosen in the Snooze UI, while keeping validation for quick actions elsewhere. snoozeUntilDate = duration > 0 ? Date().addingTimeInterval(duration) : .distantPast diff --git a/Trio/Sources/Modules/Snooze/View/SnoozeRootView.swift b/Trio/Sources/Modules/Snooze/View/SnoozeRootView.swift index 180ccccf1ea..a102832b770 100644 --- a/Trio/Sources/Modules/Snooze/View/SnoozeRootView.swift +++ b/Trio/Sources/Modules/Snooze/View/SnoozeRootView.swift @@ -36,7 +36,7 @@ extension Snooze { } private func formatInterval(_ interval: TimeInterval) -> String { - formatter.string(from: interval)! + formatter.string(from: interval) ?? "" } func getSnoozeDescription() -> String { @@ -68,7 +68,7 @@ extension Snooze { VStack(alignment: .leading) { Button { let interval = pickerTimes[selectedInterval] - let snoozeFor = formatter.string(from: interval)! + let snoozeFor = formatInterval(interval) let untilDate = Date() + interval Task { @MainActor [weak state] in From 84d8d8e5c1f99b4de8d7651ec015c758814fddd9 Mon Sep 17 00:00:00 2001 From: Trio Build Bot Date: Wed, 25 Mar 2026 14:51:12 +0100 Subject: [PATCH 43/73] Refactor snooze handling in SnoozeStateModel and UserNotificationsManager to eliminate unnecessary Task usage and improve date handling logic for snooze duration. --- Trio/Sources/Modules/Snooze/SnoozeStateModel.swift | 6 ++---- .../UserNotifications/UserNotificationsManager.swift | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Trio/Sources/Modules/Snooze/SnoozeStateModel.swift b/Trio/Sources/Modules/Snooze/SnoozeStateModel.swift index 1ff5beba6fb..83cb5886d10 100644 --- a/Trio/Sources/Modules/Snooze/SnoozeStateModel.swift +++ b/Trio/Sources/Modules/Snooze/SnoozeStateModel.swift @@ -38,9 +38,7 @@ extension Snooze { extension Snooze.StateModel: SnoozeObserver { func snoozeDidChange(_ untilDate: Date) { - Task { @MainActor in - snoozeUntilDate = untilDate - alarm = glucoseStorage.alarm - } + snoozeUntilDate = untilDate + alarm = glucoseStorage.alarm } } diff --git a/Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift b/Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift index 6e14f3630c6..be736a1825d 100644 --- a/Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift +++ b/Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift @@ -471,7 +471,7 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In } @MainActor func applySnooze(for duration: TimeInterval) async { - let untilDate = Date().addingTimeInterval(duration) + let untilDate = duration > 0 ? Date().addingTimeInterval(duration) : .distantPast snoozeUntilDate = untilDate lastGlucoseAlertToken = "" // removeGlucoseNotifications() is safe to call here since we're @MainActor From e0c35fd0f8208f6d4196d11cd835da5ac7a8520c Mon Sep 17 00:00:00 2001 From: Sam King Date: Wed, 25 Mar 2026 08:13:44 -0700 Subject: [PATCH 44/73] Remove smoothed glucose default value This commit removes the default value for smoothed glucose properties stored in CoreData. Since this property is optional, we don't need to have a default value and in fact having a default value complicates logic and requires the use of `smoothedGlucose != 0` checks. I added a missing `smoothedGlucose != 0` as a precaution in case any default values were persisted in CoreData. --- .../TrioCoreDataPersistentContainer.xcdatamodel/contents | 4 ++-- Trio/Sources/APS/OpenAPS/OpenAPS.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Model/TrioCoreDataPersistentContainer.xcdatamodeld/TrioCoreDataPersistentContainer.xcdatamodel/contents b/Model/TrioCoreDataPersistentContainer.xcdatamodeld/TrioCoreDataPersistentContainer.xcdatamodel/contents index c0a3461b4c9..332c3373a55 100644 --- a/Model/TrioCoreDataPersistentContainer.xcdatamodeld/TrioCoreDataPersistentContainer.xcdatamodel/contents +++ b/Model/TrioCoreDataPersistentContainer.xcdatamodeld/TrioCoreDataPersistentContainer.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -78,7 +78,7 @@ - + diff --git a/Trio/Sources/APS/OpenAPS/OpenAPS.swift b/Trio/Sources/APS/OpenAPS/OpenAPS.swift index 030023d62cf..14309f5ba5a 100644 --- a/Trio/Sources/APS/OpenAPS/OpenAPS.swift +++ b/Trio/Sources/APS/OpenAPS/OpenAPS.swift @@ -132,7 +132,7 @@ final class OpenAPS { return glucoseResults.map { glucose -> AlgorithmGlucose in let glucoseValue: Int16 - if shouldSmoothGlucose, !glucose.isManual, let smoothedGlucose = glucose.smoothedGlucose { + if shouldSmoothGlucose, !glucose.isManual, let smoothedGlucose = glucose.smoothedGlucose, smoothedGlucose != 0 { glucoseValue = smoothedGlucose.rounding(accordingToBehavior: roundingBehavior).int16Value } else { glucoseValue = glucose.glucose From 27f0e459e6c73171a95570d5cafc075669ee4b4a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 25 Mar 2026 16:03:15 +0000 Subject: [PATCH 45/73] CI: Bump APP_DEV_VERSION to 0.6.0.65 [skip ci] --- Config.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Config.xcconfig b/Config.xcconfig index f192da1c1fa..86d21941345 100644 --- a/Config.xcconfig +++ b/Config.xcconfig @@ -19,7 +19,7 @@ TRIO_APP_GROUP_ID = group.org.nightscout.$(DEVELOPMENT_TEAM).trio.trio-app-group // The developers set the version numbers, please leave them alone APP_VERSION = 0.6.0 -APP_DEV_VERSION = 0.6.0.64 +APP_DEV_VERSION = 0.6.0.65 APP_BUILD_NUMBER = 1 COPYRIGHT_NOTICE = From ed6d593373327744a335b639ab7f78ba3a3e4671 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 26 Mar 2026 01:45:42 +0000 Subject: [PATCH 46/73] CI: Bump APP_DEV_VERSION to 0.6.0.66 [skip ci] --- Config.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Config.xcconfig b/Config.xcconfig index 86d21941345..e5d9322441e 100644 --- a/Config.xcconfig +++ b/Config.xcconfig @@ -19,7 +19,7 @@ TRIO_APP_GROUP_ID = group.org.nightscout.$(DEVELOPMENT_TEAM).trio.trio-app-group // The developers set the version numbers, please leave them alone APP_VERSION = 0.6.0 -APP_DEV_VERSION = 0.6.0.65 +APP_DEV_VERSION = 0.6.0.66 APP_BUILD_NUMBER = 1 COPYRIGHT_NOTICE = From 924bf1c5ba8fab589dd43d66444dd7dd9d64d782 Mon Sep 17 00:00:00 2001 From: Mike Plante <82073483+MikePlante1@users.noreply.github.com> Date: Thu, 26 Mar 2026 12:25:01 -0400 Subject: [PATCH 47/73] Add watch notification files and snooze strings Add WatchNotificationHandler.swift and NotificationIdentifiers.swift to the project and include them in the Watch App Extension groups/sources (project.pbxproj updated). Add localization entries for snooze durations ("20 min", "1 hour", "3 hours"). Also apply small code/formatting tweaks: use a comma in the session reachability conditional in WatchNotificationHandler and adjust inline comments/spacing in SnoozeRootView. --- .../Helper/WatchNotificationHandler.swift | 2 +- Trio.xcodeproj/project.pbxproj | 10 ++++++++++ Trio/Sources/Localizations/Main/Localizable.xcstrings | 9 +++++++++ Trio/Sources/Modules/Snooze/View/SnoozeRootView.swift | 8 ++++---- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/Trio Watch App Extension/Helper/WatchNotificationHandler.swift b/Trio Watch App Extension/Helper/WatchNotificationHandler.swift index 112431e2acb..7a76bd88173 100644 --- a/Trio Watch App Extension/Helper/WatchNotificationHandler.swift +++ b/Trio Watch App Extension/Helper/WatchNotificationHandler.swift @@ -53,7 +53,7 @@ final class WatchNotificationHandler: NSObject, UNUserNotificationCenterDelegate // Try sendMessage first if session is reachable and activated (faster, immediate delivery) // Fall back to transferUserInfo if not reachable or if sendMessage fails - if session.isReachable && session.activationState == .activated { + if session.isReachable, session.activationState == .activated { session.sendMessage(payload, replyHandler: nil) { _ in // Fallback to transferUserInfo if sendMessage fails session.transferUserInfo(payload) diff --git a/Trio.xcodeproj/project.pbxproj b/Trio.xcodeproj/project.pbxproj index fe0aeb3472a..9be28402828 100644 --- a/Trio.xcodeproj/project.pbxproj +++ b/Trio.xcodeproj/project.pbxproj @@ -461,6 +461,9 @@ C2AA6CF72E1A734A00BF6C16 /* SettingsExportProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2AA6CF32E1A734A00BF6C16 /* SettingsExportProvider.swift */; }; C2AA6CF82E1A734A00BF6C16 /* SettingsExportDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2AA6CF22E1A734A00BF6C16 /* SettingsExportDataFlow.swift */; }; C2AA6CF92E1A734A00BF6C16 /* SettingsExportStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2AA6CF42E1A734A00BF6C16 /* SettingsExportStateModel.swift */; }; + C2BA6B972F758E7500348E6A /* WatchNotificationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2BA6B962F758E7500348E6A /* WatchNotificationHandler.swift */; }; + C2BA6B992F758E7600348E6A /* NotificationIdentifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2BA6B982F758E7600348E6A /* NotificationIdentifiers.swift */; }; + C2BA6B9A2F7593C300348E6A /* NotificationIdentifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2BA6B982F758E7600348E6A /* NotificationIdentifiers.swift */; }; C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFCFE0781F9074C2917890E8 /* ManualTempBasalStateModel.swift */; }; CA370FC152BC98B3D1832968 /* BasalProfileEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */; }; CC6C406E2ACDD69E009B8058 /* RawFetchedProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC6C406D2ACDD69E009B8058 /* RawFetchedProfile.swift */; }; @@ -1296,6 +1299,8 @@ C2AA6CF22E1A734A00BF6C16 /* SettingsExportDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsExportDataFlow.swift; sourceTree = ""; }; C2AA6CF32E1A734A00BF6C16 /* SettingsExportProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsExportProvider.swift; sourceTree = ""; }; C2AA6CF42E1A734A00BF6C16 /* SettingsExportStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsExportStateModel.swift; sourceTree = ""; }; + C2BA6B962F758E7500348E6A /* WatchNotificationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchNotificationHandler.swift; sourceTree = ""; }; + C2BA6B982F758E7600348E6A /* NotificationIdentifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationIdentifiers.swift; sourceTree = ""; }; C377490C77661D75E8C50649 /* ManualTempBasalRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalRootView.swift; sourceTree = ""; }; C8D1A7CA8C10C4403D4BBFA7 /* TreatmentsDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TreatmentsDataFlow.swift; sourceTree = ""; }; CC6C406D2ACDD69E009B8058 /* RawFetchedProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RawFetchedProfile.swift; sourceTree = ""; }; @@ -2433,6 +2438,7 @@ DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */, DD9ECB692CA99F6C00AA7C45 /* CommandPayload.swift */, DDD6D4D22CDE90720029439A /* EstimatedA1cDisplayUnit.swift */, + C2BA6B982F758E7600348E6A /* NotificationIdentifiers.swift */, ); path = Models; sourceTree = ""; @@ -3384,6 +3390,7 @@ DD09D5C62D29EB26000D82C9 /* Helper+Enums.swift */, DD3A3CE82D29C97800AE478E /* Helper+ButtonStyles.swift */, DD3A3CE62D29C93F00AE478E /* Helper+Extensions.swift */, + C2BA6B962F758E7500348E6A /* WatchNotificationHandler.swift */, ); path = Helper; sourceTree = ""; @@ -4505,6 +4512,7 @@ 53F2382465BF74DB1A967C8B /* PumpConfigProvider.swift in Sources */, 5D16287A969E64D18CE40E44 /* PumpConfigStateModel.swift in Sources */, DDF6902C2DA028D3008BF16C /* DiagnosticsStepView.swift in Sources */, + C2BA6B992F758E7600348E6A /* NotificationIdentifiers.swift in Sources */, 19D466AA29AA3099004D5F33 /* MealSettingsRootView.swift in Sources */, CEF1ED6B2D58FB5800FAF41E /* CGMOptions.swift in Sources */, E974172296125A5AE99E634C /* PumpConfigRootView.swift in Sources */, @@ -4810,6 +4818,7 @@ BDA25F202D26D5FE00035F34 /* CarbsInputView.swift in Sources */, BDA25F1C2D26BD0700035F34 /* TrendShape.swift in Sources */, BDFF7A892D25F97D0016C40C /* TrioWatchApp.swift in Sources */, + C2BA6B972F758E7500348E6A /* WatchNotificationHandler.swift in Sources */, DD09D5C72D29EB2F000D82C9 /* Helper+Enums.swift in Sources */, BD54A9742D281AEF00F9C1EE /* TempTargetPresetWatch.swift in Sources */, BD54A95C2D2808A300F9C1EE /* OverridePresetWatch.swift in Sources */, @@ -4820,6 +4829,7 @@ DD3A3CE72D29C93F00AE478E /* Helper+Extensions.swift in Sources */, DD246F062D2836AA0027DDE0 /* GlucoseTrendView.swift in Sources */, BD432CA22D2F4E4000D1EB79 /* WatchMessageKeys.swift in Sources */, + C2BA6B9A2F7593C300348E6A /* NotificationIdentifiers.swift in Sources */, DD8262CB2D289297009F6F62 /* BolusConfirmationView.swift in Sources */, BD54A9712D281A8100F9C1EE /* TempTargetPresetsView.swift in Sources */, DD3A3CE92D29C97800AE478E /* Helper+ButtonStyles.swift in Sources */, diff --git a/Trio/Sources/Localizations/Main/Localizable.xcstrings b/Trio/Sources/Localizations/Main/Localizable.xcstrings index 0754b119aef..f9d6f55ef56 100644 --- a/Trio/Sources/Localizations/Main/Localizable.xcstrings +++ b/Trio/Sources/Localizations/Main/Localizable.xcstrings @@ -20236,6 +20236,9 @@ "comment" : "Name of the bolus display threshold option that represents 0.5 units.", "isCommentAutoGenerated" : true }, + "1 hour" : { + "comment" : "Snooze glucose alerts for 1 hour" + }, "1 U and over" : { "comment" : "Name to display for the \"1 U and over\" bolus display threshold option.", "isCommentAutoGenerated" : true @@ -20719,6 +20722,9 @@ } } }, + "3 hours" : { + "comment" : "Snooze glucose alerts for 3 hours" + }, "3 M" : { "comment" : "Abbreviation for three months", "localizations" : { @@ -22157,6 +22163,9 @@ } } }, + "20 min" : { + "comment" : "Snooze glucose alerts for 20 minutes" + }, "24 hours" : { "extractionState" : "manual", "localizations" : { diff --git a/Trio/Sources/Modules/Snooze/View/SnoozeRootView.swift b/Trio/Sources/Modules/Snooze/View/SnoozeRootView.swift index a102832b770..6ad4641cf30 100644 --- a/Trio/Sources/Modules/Snooze/View/SnoozeRootView.swift +++ b/Trio/Sources/Modules/Snooze/View/SnoozeRootView.swift @@ -15,10 +15,10 @@ extension Snooze { private var pickerTimes: [TimeInterval] { [ - TimeInterval(minutes: 20), // 20 minutes - TimeInterval(hours: 1), // 1 hour - TimeInterval(hours: 3), // 3 hours - TimeInterval(hours: 6) // 6 hours + TimeInterval(minutes: 20), // 20 minutes + TimeInterval(hours: 1), // 1 hour + TimeInterval(hours: 3), // 3 hours + TimeInterval(hours: 6) // 6 hours ] } From 42fd5a389a4576f57055b022b61592ae78d693d8 Mon Sep 17 00:00:00 2001 From: Mike Plante <82073483+MikePlante1@users.noreply.github.com> Date: Thu, 26 Mar 2026 23:31:42 -0400 Subject: [PATCH 48/73] Fix typo --- .../Localizations/Main/Localizable.xcstrings | 90 +++---------------- .../Glucose/GlucoseSectorChart.swift | 2 +- 2 files changed, 11 insertions(+), 81 deletions(-) diff --git a/Trio/Sources/Localizations/Main/Localizable.xcstrings b/Trio/Sources/Localizations/Main/Localizable.xcstrings index 0754b119aef..63fcbbbe41c 100644 --- a/Trio/Sources/Localizations/Main/Localizable.xcstrings +++ b/Trio/Sources/Localizations/Main/Localizable.xcstrings @@ -265731,120 +265731,50 @@ } } }, - "Very Low (<%@" : { + "Very Low (<%@)" : { + "comment" : "A description of a glucose range.", + "isCommentAutoGenerated" : true, "localizations" : { - "bg" : { - "stringUnit" : { - "state" : "new", - "value" : "Very Low (<%@" - } - }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Meget Lav (<%@" + "value" : "Meget Lav (<%@)" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Sehr niedrig (<%@" - } - }, - "es" : { - "stringUnit" : { - "state" : "new", - "value" : "Very Low (<%@" + "value" : "Sehr niedrig (<%@)" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Très bas (<%@" - } - }, - "he" : { - "stringUnit" : { - "state" : "new", - "value" : "Very Low (<%@" - } - }, - "it" : { - "stringUnit" : { - "state" : "new", - "value" : "Very Low (<%@" - } - }, - "ko" : { - "stringUnit" : { - "state" : "new", - "value" : "Very Low (<%@" + "value" : "Très bas (<%@)" } }, "nb-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Veldig lavt (<%@" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Very Low (<%@" + "value" : "Veldig lavt (<%@)" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Bardzo niski (<%@" - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "new", - "value" : "Very Low (<%@" - } - }, - "ro" : { - "stringUnit" : { - "state" : "new", - "value" : "Very Low (<%@" - } - }, - "ru" : { - "stringUnit" : { - "state" : "new", - "value" : "Very Low (<%@" - } - }, - "sv" : { - "stringUnit" : { - "state" : "new", - "value" : "Very Low (<%@" - } - }, - "tr" : { - "stringUnit" : { - "state" : "new", - "value" : "Very Low (<%@" - } - }, - "uk" : { - "stringUnit" : { - "state" : "new", - "value" : "Very Low (<%@" + "value" : "Bardzo niski (<%@)" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Qua thấp (<%@" + "value" : "Qua thấp (<%@)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "非常低(<%@" + "value" : "非常低(<%@)" } } } diff --git a/Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseSectorChart.swift b/Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseSectorChart.swift index 59344c9be6d..b460bfb5575 100644 --- a/Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseSectorChart.swift +++ b/Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseSectorChart.swift @@ -343,7 +343,7 @@ struct GlucoseSectorChart: View { formatPercentage(Decimal(low) / total * 100) ), ( - String(localized: "Very Low (<\(Decimal(54).formatted(for: units))"), + String(localized: "Very Low (<\(Decimal(54).formatted(for: units)))"), formatPercentage(Decimal(veryLow) / total * 100) ), (String(localized: "Average"), average.formatted(for: units)), From 1aa3c2bf9e985e9f1db92b420c6dfa88c9d79003 Mon Sep 17 00:00:00 2001 From: Mike Plante <82073483+MikePlante1@users.noreply.github.com> Date: Fri, 27 Mar 2026 10:02:03 -0400 Subject: [PATCH 49/73] Show start of every month on chart axes for 3M stats views --- .../Stat/View/ViewElements/Insulin/BolusStatsView.swift | 4 ++-- .../Stat/View/ViewElements/Insulin/TotalDailyDoseChart.swift | 4 ++-- .../Modules/Stat/View/ViewElements/Meal/MealStatsView.swift | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Trio/Sources/Modules/Stat/View/ViewElements/Insulin/BolusStatsView.swift b/Trio/Sources/Modules/Stat/View/ViewElements/Insulin/BolusStatsView.swift index 800d2aa8663..4ef55a0ecf0 100644 --- a/Trio/Sources/Modules/Stat/View/ViewElements/Insulin/BolusStatsView.swift +++ b/Trio/Sources/Modules/Stat/View/ViewElements/Insulin/BolusStatsView.swift @@ -272,9 +272,9 @@ struct BolusStatsView: View { AxisGridLine() } case .total: - // Only show every other month + // Show start of every month let day = Calendar.current.component(.day, from: date) - if day == 1 && Calendar.current.component(.month, from: date) % 2 == 1 { + if day == 1 { AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true) .font(.footnote) AxisGridLine() diff --git a/Trio/Sources/Modules/Stat/View/ViewElements/Insulin/TotalDailyDoseChart.swift b/Trio/Sources/Modules/Stat/View/ViewElements/Insulin/TotalDailyDoseChart.swift index 3ac856f89c3..03f684258fd 100644 --- a/Trio/Sources/Modules/Stat/View/ViewElements/Insulin/TotalDailyDoseChart.swift +++ b/Trio/Sources/Modules/Stat/View/ViewElements/Insulin/TotalDailyDoseChart.swift @@ -224,8 +224,8 @@ struct TotalDailyDoseChart: View { AxisGridLine() } case .total: - // Only show every other month - if day == 1 && Calendar.current.component(.month, from: date) % 2 == 1 { + // Show start of every month + if day == 1 { AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true) .font(.footnote) AxisGridLine() diff --git a/Trio/Sources/Modules/Stat/View/ViewElements/Meal/MealStatsView.swift b/Trio/Sources/Modules/Stat/View/ViewElements/Meal/MealStatsView.swift index 761b5e22c57..d707efacfcf 100644 --- a/Trio/Sources/Modules/Stat/View/ViewElements/Meal/MealStatsView.swift +++ b/Trio/Sources/Modules/Stat/View/ViewElements/Meal/MealStatsView.swift @@ -254,9 +254,9 @@ struct MealStatsView: View { AxisGridLine() } case .total: - // Only show every other month + // Show start of every month let day = Calendar.current.component(.day, from: date) - if day == 1 && Calendar.current.component(.month, from: date) % 2 == 1 { + if day == 1 { AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true) .font(.footnote) AxisGridLine() From 02e9c22958b1e7199692f4bf8cbd5d36487196d0 Mon Sep 17 00:00:00 2001 From: Sam King Date: Fri, 27 Mar 2026 09:03:33 -0700 Subject: [PATCH 50/73] Fix crash from force-unwrap on orphaned PumpEventStored with nil bolus This commit removes a force-unwrap from our pump event pipeline and replaces it with more idiomatic swift optional handling. The net effect of a pump event of type `bolus` that doesn't have a corresponding `bolus` property is that it'll show up in the history UI as a blank bolus but these events will be filtered out of the pump history before passing them to the algorithm. Without this fix, when one of these pump events sneaks into an individual's pump history it will cause their app to crash before doing anything, so they have to either wait 24 hours for the pump event to age out or delete trio and start over. With the fix, it will filter these events out in a way that is consistent with the existing code for pump event handling. --- Trio/Sources/APS/Storage/PumpHistoryStorage.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Trio/Sources/APS/Storage/PumpHistoryStorage.swift b/Trio/Sources/APS/Storage/PumpHistoryStorage.swift index 43670197e93..1742a366ca0 100644 --- a/Trio/Sources/APS/Storage/PumpHistoryStorage.swift +++ b/Trio/Sources/APS/Storage/PumpHistoryStorage.swift @@ -291,13 +291,16 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable { } func determineBolusEventType(for event: PumpEventStored) -> PumpEventStored.EventType { - if event.bolus!.isSMB { + guard let bolus = event.bolus else { + return event.type.flatMap({ PumpEventStored.EventType(rawValue: $0) }) ?? .bolus + } + if bolus.isSMB { return .smb } - if event.bolus!.isExternal { + if bolus.isExternal { return .isExternal } - return PumpEventStored.EventType(rawValue: event.type!) ?? PumpEventStored.EventType.bolus + return event.type.flatMap({ PumpEventStored.EventType(rawValue: $0) }) ?? .bolus } func getPumpHistoryNotYetUploadedToNightscout() async throws -> [NightscoutTreatment] { From 50e1d459e2e4603c3dea529c03f69f7c7c25e26f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 27 Mar 2026 16:23:59 +0000 Subject: [PATCH 51/73] CI: Bump APP_DEV_VERSION to 0.6.0.67 [skip ci] --- Config.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Config.xcconfig b/Config.xcconfig index e5d9322441e..2ccab223661 100644 --- a/Config.xcconfig +++ b/Config.xcconfig @@ -19,7 +19,7 @@ TRIO_APP_GROUP_ID = group.org.nightscout.$(DEVELOPMENT_TEAM).trio.trio-app-group // The developers set the version numbers, please leave them alone APP_VERSION = 0.6.0 -APP_DEV_VERSION = 0.6.0.66 +APP_DEV_VERSION = 0.6.0.67 APP_BUILD_NUMBER = 1 COPYRIGHT_NOTICE = From d63c70a2c8c7e85b77fd37db96456c8e21c2366e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 27 Mar 2026 16:29:03 +0000 Subject: [PATCH 52/73] CI: Bump APP_DEV_VERSION to 0.6.0.68 [skip ci] --- Config.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Config.xcconfig b/Config.xcconfig index 2ccab223661..f9f649a510e 100644 --- a/Config.xcconfig +++ b/Config.xcconfig @@ -19,7 +19,7 @@ TRIO_APP_GROUP_ID = group.org.nightscout.$(DEVELOPMENT_TEAM).trio.trio-app-group // The developers set the version numbers, please leave them alone APP_VERSION = 0.6.0 -APP_DEV_VERSION = 0.6.0.67 +APP_DEV_VERSION = 0.6.0.68 APP_BUILD_NUMBER = 1 COPYRIGHT_NOTICE = From 43bb419ca6a084119f71f65d8f3541f09fa07223 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 27 Mar 2026 16:43:15 +0000 Subject: [PATCH 53/73] CI: Bump APP_DEV_VERSION to 0.6.0.69 [skip ci] --- Config.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Config.xcconfig b/Config.xcconfig index f9f649a510e..b2ce0f067b0 100644 --- a/Config.xcconfig +++ b/Config.xcconfig @@ -19,7 +19,7 @@ TRIO_APP_GROUP_ID = group.org.nightscout.$(DEVELOPMENT_TEAM).trio.trio-app-group // The developers set the version numbers, please leave them alone APP_VERSION = 0.6.0 -APP_DEV_VERSION = 0.6.0.68 +APP_DEV_VERSION = 0.6.0.69 APP_BUILD_NUMBER = 1 COPYRIGHT_NOTICE = From ff6574280daf6102202ed9f12fafe88efd9de24f Mon Sep 17 00:00:00 2001 From: Deniz Cengiz Date: Sat, 28 Mar 2026 22:46:39 +0100 Subject: [PATCH 54/73] Rework and fix smoothing stop gap issue --- Trio/Sources/APS/FetchGlucoseManager.swift | 71 ++++++++++++---------- TrioTests/GlucoseSmoothingTests.swift | 70 +++++++++++++-------- 2 files changed, 82 insertions(+), 59 deletions(-) diff --git a/Trio/Sources/APS/FetchGlucoseManager.swift b/Trio/Sources/APS/FetchGlucoseManager.swift index 37a03230c2c..691b5e826c3 100644 --- a/Trio/Sources/APS/FetchGlucoseManager.swift +++ b/Trio/Sources/APS/FetchGlucoseManager.swift @@ -240,37 +240,6 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable { return Manager.init(rawState: rawState) } - func fetchGlucose(context: NSManagedObjectContext) async throws -> [NSManagedObjectID] { - // Compound predicate: time window + non-manual + valid date - let timePredicate = NSPredicate.predicateForOneDayAgoInMinutes - let manualPredicate = NSPredicate(format: "isManual == NO") - let datePredicate = NSPredicate(format: "date != nil") - - let compoundPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ - timePredicate, - manualPredicate, - datePredicate - ]) - - let results = try await CoreDataStack.shared.fetchEntitiesAsync( - ofType: GlucoseStored.self, - onContext: context, - // Predicate must cover at least the full glucose horizon used by downstream algorithm consumers. - // If autosens / oref / smoothing logic ever starts looking back further (e.g. 36h), - // this fetch window must be expanded accordingly. - predicate: compoundPredicate, - key: "date", - ascending: true, // the first element is the oldest - fetchLimit: 350 - ) - - guard let glucoseArray = results as? [GlucoseStored] else { - throw CoreDataError.fetchError(function: #function, file: #file) - } - - return glucoseArray.map(\.objectID) - } - private func glucoseStoreAndHeartDecision(syncDate: Date, glucose: [BloodGlucose]) async throws { // calibration add if required only for sensor let newGlucose = overcalibrate(entries: glucose) @@ -394,6 +363,37 @@ extension BaseFetchGlucoseManager: SettingsObserver { } extension BaseFetchGlucoseManager { + func fetchGlucose(context: NSManagedObjectContext) async throws -> [NSManagedObjectID] { + // Compound predicate: time window + non-manual + valid date + let timePredicate = NSPredicate.predicateForFourHoursAgo + let manualPredicate = NSPredicate(format: "isManual == NO") + let datePredicate = NSPredicate(format: "date != nil") + + let compoundPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ + timePredicate, + manualPredicate, + datePredicate + ]) + + let results = try await CoreDataStack.shared.fetchEntitiesAsync( + ofType: GlucoseStored.self, + onContext: context, + // Predicate must cover at least the full glucose horizon used by downstream algorithm consumers. + // If autosens / oref / smoothing logic ever starts looking back further (e.g. 36h), + // this fetch window must be expanded accordingly. + predicate: compoundPredicate, + key: "date", + ascending: true, // the first element is the oldest + fetchLimit: 350 + ) + + guard let glucoseArray = results as? [GlucoseStored] else { + throw CoreDataError.fetchError(function: #function, file: #file) + } + + return glucoseArray.map(\.objectID) + } + /// CoreData-friendly AAPS exponential smoothing + storage. /// - Important: Only stores `smoothedGlucose`. UI/alerts should still use `glucose`. /// @@ -474,12 +474,17 @@ extension BaseFetchGlucoseManager { } } - // If insufficient valid readings: copy raw into smoothed (clamped) for all passed entries. + // Not enough recent contiguous readings to smooth (e.g. after CGM gap). + // IMPORTANT: Only apply fallback to the recent window, not all data. + // Otherwise a recent gap would overwrite historical smoothed values. guard validWindowCount >= minimumWindowSize else { - for object in data { + let recentWindow = data.suffix(validWindowCount) + + for object in recentWindow { let raw = Decimal(Int(object.glucose)) object.smoothedGlucose = max(raw, minimumSmoothedGlucose) as NSDecimalNumber } + return } diff --git a/TrioTests/GlucoseSmoothingTests.swift b/TrioTests/GlucoseSmoothingTests.swift index 4bfb03595b5..79e8d995e7b 100644 --- a/TrioTests/GlucoseSmoothingTests.swift +++ b/TrioTests/GlucoseSmoothingTests.swift @@ -120,46 +120,64 @@ import Testing } @Test( - "Exponential smoothing stops window at gaps >= 12 minutes; fallback fills smoothed glucose" - ) func testExponentialSmoothingGapStopsWindow() async throws { - // GIVEN: + "Exponential smoothing stops at gaps >= 12 minutes and only updates the most recent window" + ) func testExponentialSmoothingGapStopsWindow() async throws { let now = Date() - let dates: [Date] = [ - now.addingTimeInterval(0), // oldest - now.addingTimeInterval(5 * 60), - now.addingTimeInterval(10 * 60), - now.addingTimeInterval(25 * 60), // gap of 15 minutes - now.addingTimeInterval(30 * 60), - now.addingTimeInterval(35 * 60) // newest - ] - let values: [Int16] = [100, 105, 110, 115, 120, 125] + + var dates: [Date] = [] + var values: [Int16] = [] + + // Older contiguous block (should remain untouched) + for i in 0 ..< 10 { + dates.append(now.addingTimeInterval(Double(i) * 5 * 60)) + values.append(Int16(100 + i * 5)) + } + + // GAP (15 minutes) + let gapStart = now.addingTimeInterval(Double(10) * 5 * 60 + 15 * 60) + + // Recent block (too small -> fallback applies only here) + for i in 0 ..< 3 { + dates.append(gapStart.addingTimeInterval(Double(i) * 5 * 60)) + values.append(Int16(200 + i * 5)) + } + await createGlucoseSequence(values: values, dates: dates, isManual: false) - // WHEN await fetchGlucoseManager.exponentialSmoothingGlucose(context: testContext) - // THEN let ascending = try await fetchAndSortGlucose() - #expect(ascending.count == 6) + #expect(ascending.count == values.count) - let smoothedValues = ascending - .filter { !$0.isManual } - .compactMap { $0.smoothedGlucose?.decimalValue } - .filter { $0 > 0 } + // Split into: + // - older block (before gap) + // - recent block (after gap) + let olderBlock = ascending.prefix(10) + let recentBlock = ascending.suffix(3) - #expect( - smoothedValues.count == 6, - "Fallback path should fill smoothedGlucose for all CGM entries when the gap reduces the window below minimum size." - ) + // --- ASSERT 1: Older values should NOT be overwritten --- + for (index, obj) in olderBlock.enumerated() { + #expect( + obj.smoothedGlucose == nil, + "Older value at index \(index) should remain untouched (no fallback overwrite)." + ) + } + + // --- ASSERT 2: Recent values should be filled by fallback --- + for (index, obj) in recentBlock.enumerated() { + guard let smoothed = obj.smoothedGlucose?.decimalValue else { + #expect(false, "Recent value at index \(index) should have smoothedGlucose set.") + continue + } - for (index, smoothed) in smoothedValues.enumerated() { #expect( smoothed >= 39, - "Fallback smoothed glucose must be clamped to >= 39, got \(smoothed) at index \(index)." + "Fallback smoothed glucose must be clamped to >= 39, got \(smoothed)." ) + #expect( smoothed == smoothed.rounded(toPlaces: 0), - "Fallback smoothed glucose must be rounded to an integer, got \(smoothed) at index \(index)." + "Fallback smoothed glucose must be rounded to integer, got \(smoothed)." ) } } From 7f55ef0612d7465dc3ffe5055d3cccb76e837b18 Mon Sep 17 00:00:00 2001 From: Deniz Cengiz <48965855+dnzxy@users.noreply.github.com> Date: Sun, 29 Mar 2026 06:25:56 +0200 Subject: [PATCH 55/73] Revert predicate to fetch 24 hrs of glucose --- Trio/Sources/APS/FetchGlucoseManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Trio/Sources/APS/FetchGlucoseManager.swift b/Trio/Sources/APS/FetchGlucoseManager.swift index 691b5e826c3..3f9573d9e7a 100644 --- a/Trio/Sources/APS/FetchGlucoseManager.swift +++ b/Trio/Sources/APS/FetchGlucoseManager.swift @@ -365,7 +365,7 @@ extension BaseFetchGlucoseManager: SettingsObserver { extension BaseFetchGlucoseManager { func fetchGlucose(context: NSManagedObjectContext) async throws -> [NSManagedObjectID] { // Compound predicate: time window + non-manual + valid date - let timePredicate = NSPredicate.predicateForFourHoursAgo + let timePredicate = NSPredicate.predicateForOneDayAgoInMinutes let manualPredicate = NSPredicate(format: "isManual == NO") let datePredicate = NSPredicate(format: "date != nil") From 7d53d938688526f66b08e446335daa94ad71589d Mon Sep 17 00:00:00 2001 From: Deniz Cengiz Date: Sun, 29 Mar 2026 21:03:31 +0200 Subject: [PATCH 56/73] Bump MedtrumKit --- MedtrumKit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MedtrumKit b/MedtrumKit index 43ef0424a85..a4e8533ee9a 160000 --- a/MedtrumKit +++ b/MedtrumKit @@ -1 +1 @@ -Subproject commit 43ef0424a8562aae0cf8995511b3b5d559240c16 +Subproject commit a4e8533ee9a2ce1aa2ea80cf966189f614b44834 From fec67f699e35fe442754353e3b2b65d5e166f9f3 Mon Sep 17 00:00:00 2001 From: Deniz Cengiz Date: Sun, 29 Mar 2026 21:03:52 +0200 Subject: [PATCH 57/73] Adjust remaining time string handling after rework --- Trio/Sources/APS/DeviceDataManager.swift | 38 ++++++++++--------- .../Modules/Home/View/Header/PumpView.swift | 13 +++---- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/Trio/Sources/APS/DeviceDataManager.swift b/Trio/Sources/APS/DeviceDataManager.swift index a0d9d6b9cf9..d835c39ab74 100644 --- a/Trio/Sources/APS/DeviceDataManager.swift +++ b/Trio/Sources/APS/DeviceDataManager.swift @@ -125,17 +125,18 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable { } pumpExpiresAtDate.send(endTime) } - if let medtrum = pumpManager as? MedtrumPumpManager { - if medtrum.state.expirationTimer == 1 { - pumpActivatedAtDate.send(nil) - guard let endTime = medtrum.state.patchExpiresAt else { - pumpExpiresAtDate.send(nil) - return - } - pumpExpiresAtDate.send(endTime) - } else { - pumpActivatedAtDate.send(medtrum.state.patchActivatedAt) + if let medtrumPump = pumpManager as? MedtrumPumpManager { + guard let endTime = medtrumPump.state.patchExpiresAt else { pumpExpiresAtDate.send(nil) + return + } + pumpExpiresAtDate.send(endTime) + + switch medtrumPump.state.expiryMode { + case .default: + pumpActivatedAtDate.send(nil) + case .extended: + pumpActivatedAtDate.send(medtrumPump.state.patchActivatedAt) } } if let simulatorPump = pumpManager as? MockPumpManager { @@ -533,16 +534,17 @@ extension BaseDeviceDataManager: PumpManagerDelegate { $0.pumpReservoirDidChange(Decimal(medtrumPump.state.reservoir)) } - if medtrumPump.state.expirationTimer == 1 { + guard let endTime = medtrumPump.state.patchExpiresAt else { + pumpExpiresAtDate.send(nil) + return + } + pumpExpiresAtDate.send(endTime) + + switch medtrumPump.state.expiryMode { + case .default: pumpActivatedAtDate.send(nil) - guard let endTime = medtrumPump.state.patchExpiresAt else { - pumpExpiresAtDate.send(nil) - return - } - pumpExpiresAtDate.send(endTime) - } else { + case .extended: pumpActivatedAtDate.send(medtrumPump.state.patchActivatedAt) - pumpExpiresAtDate.send(nil) } } diff --git a/Trio/Sources/Modules/Home/View/Header/PumpView.swift b/Trio/Sources/Modules/Home/View/Header/PumpView.swift index 401c498abe7..c49093764b9 100644 --- a/Trio/Sources/Modules/Home/View/Header/PumpView.swift +++ b/Trio/Sources/Modules/Home/View/Header/PumpView.swift @@ -1,4 +1,5 @@ import CoreData +import MedtrumKit import SwiftUI struct PumpView: View { @@ -100,26 +101,24 @@ struct PumpView: View { } if let date = expiresAtDate { - PatchTimer(date, isExpiration: true) + PatchTimer(date, expiryMode: ExpiryMode.default) } if let date = activatedAtDate { - PatchTimer(date, isExpiration: false) + PatchTimer(date, expiryMode: ExpiryMode.extended) } } } } - @ViewBuilder private func PatchTimer(_ date: Date, isExpiration: Bool) -> some View { + @ViewBuilder private func PatchTimer(_ date: Date, expiryMode: ExpiryMode) -> some View { HStack { Image(systemName: hourglassIcon) .font(.callout) .foregroundStyle(timerColor, timerColorSecondary) .symbolRenderingMode(.palette) - let remainingTimeString = isExpiration ? - remainingTimeString(time: date.timeIntervalSince(timerDate)) : - activeTimeString(time: timerDate.timeIntervalSince(date)) + let remainingTimeString = remainingTimeString(time: date.timeIntervalSince(timerDate)) Text(remainingTimeString) .font(date.timeIntervalSince(timerDate) > 0 ? .callout : .subheadline) @@ -135,7 +134,7 @@ struct PumpView: View { ) } // aligns the stopwatch icon exactly with the first pixel of the reservoir icon - .padding(.leading, date.timeIntervalSince(timerDate) > 0 || !isExpiration ? 12 : 0) + .padding(.leading, date.timeIntervalSince(timerDate) > 0 || expiryMode == ExpiryMode.extended ? 12 : 0) } private func remainingTimeString(time: TimeInterval) -> String { From f17185b7688dd6722555c573d92186fd3443247e Mon Sep 17 00:00:00 2001 From: Deniz Cengiz Date: Sun, 29 Mar 2026 21:18:14 +0200 Subject: [PATCH 58/73] Remove MedtrumKit reliance in pump header view again --- Trio/Sources/Modules/Home/View/Header/PumpView.swift | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/Trio/Sources/Modules/Home/View/Header/PumpView.swift b/Trio/Sources/Modules/Home/View/Header/PumpView.swift index c49093764b9..3a2f00a6575 100644 --- a/Trio/Sources/Modules/Home/View/Header/PumpView.swift +++ b/Trio/Sources/Modules/Home/View/Header/PumpView.swift @@ -1,5 +1,4 @@ import CoreData -import MedtrumKit import SwiftUI struct PumpView: View { @@ -101,17 +100,13 @@ struct PumpView: View { } if let date = expiresAtDate { - PatchTimer(date, expiryMode: ExpiryMode.default) - } - - if let date = activatedAtDate { - PatchTimer(date, expiryMode: ExpiryMode.extended) + PatchTimer(forDate: date) } } } } - @ViewBuilder private func PatchTimer(_ date: Date, expiryMode: ExpiryMode) -> some View { + @ViewBuilder private func PatchTimer(forDate date: Date) -> some View { HStack { Image(systemName: hourglassIcon) .font(.callout) @@ -134,7 +129,7 @@ struct PumpView: View { ) } // aligns the stopwatch icon exactly with the first pixel of the reservoir icon - .padding(.leading, date.timeIntervalSince(timerDate) > 0 || expiryMode == ExpiryMode.extended ? 12 : 0) + .padding(.leading, date.timeIntervalSince(timerDate) > 0 || activatedAtDate != nil ? 12 : 0) } private func remainingTimeString(time: TimeInterval) -> String { From 8ecf9c678c07698e099dbea8b3a1b66cb83898db Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 30 Mar 2026 05:39:20 +0000 Subject: [PATCH 59/73] CI: Bump APP_DEV_VERSION to 0.6.0.70 [skip ci] --- Config.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Config.xcconfig b/Config.xcconfig index b2ce0f067b0..3c07d69accd 100644 --- a/Config.xcconfig +++ b/Config.xcconfig @@ -19,7 +19,7 @@ TRIO_APP_GROUP_ID = group.org.nightscout.$(DEVELOPMENT_TEAM).trio.trio-app-group // The developers set the version numbers, please leave them alone APP_VERSION = 0.6.0 -APP_DEV_VERSION = 0.6.0.69 +APP_DEV_VERSION = 0.6.0.70 APP_BUILD_NUMBER = 1 COPYRIGHT_NOTICE = From 32726099340f6360776d44f29bb7765ca4972557 Mon Sep 17 00:00:00 2001 From: Deniz Cengiz <48965855+dnzxy@users.noreply.github.com> Date: Mon, 30 Mar 2026 20:46:34 +0200 Subject: [PATCH 60/73] Remove unused code --- .../Modules/Home/View/Header/PumpView.swift | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/Trio/Sources/Modules/Home/View/Header/PumpView.swift b/Trio/Sources/Modules/Home/View/Header/PumpView.swift index 3a2f00a6575..c28e1511a81 100644 --- a/Trio/Sources/Modules/Home/View/Header/PumpView.swift +++ b/Trio/Sources/Modules/Home/View/Header/PumpView.swift @@ -161,23 +161,6 @@ struct PumpView: View { return "\(minutes)" + String(localized: "m", comment: "abbreviation for minutes") } - private func activeTimeString(time: TimeInterval) -> String { - var time = time - let days = Int(time / 1.days.timeInterval) - time -= days.days.timeInterval - let hours = Int(time / 1.hours.timeInterval) - time -= hours.hours.timeInterval - let minutes = Int(time / 1.minutes.timeInterval) - - if days >= 1 { - return "\(days)" + String(localized: "d", comment: "abbreviation for days") + " \(hours)" + - String(localized: "h", comment: "abbreviation for hours") - } - - return "\(hours)" + String(localized: "h", comment: "abbreviation for hours") + "\(minutes)" + - String(localized: "m", comment: "abbreviation for minutes") - } - private var batteryColor: Color { guard let battery = battery.first else { return .gray From 4ef32aaabf475e1cb9aeefe2f02e542bc84e56d5 Mon Sep 17 00:00:00 2001 From: Sam King Date: Fri, 27 Mar 2026 09:25:40 -0700 Subject: [PATCH 61/73] Fix active bolus view being obscured and isSuspended race condition Remove .ignoresSafeArea(.keyboard) from HomeRootView which could intermittently cause the bottom content (active bolus/adjustment views) to be laid out outside the visible area due to SwiftUI safe area inset miscalculation. Also, fixed a race condition in APSManager so that isSuspended is always accessed from the main dispatch queue (it's accessed by the UI). --- Trio/Sources/APS/APSManager.swift | 2 +- Trio/Sources/Modules/Home/View/HomeRootView.swift | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Trio/Sources/APS/APSManager.swift b/Trio/Sources/APS/APSManager.swift index c9d20513144..63a22e32c84 100644 --- a/Trio/Sources/APS/APSManager.swift +++ b/Trio/Sources/APS/APSManager.swift @@ -197,7 +197,7 @@ final class BaseAPSManager: APSManager, Injectable { .store(in: &lifetime) deviceDataManager.suspended - .receive(on: processQueue) + .receive(on: DispatchQueue.main) .sink { suspended in self.isSuspended = suspended } diff --git a/Trio/Sources/Modules/Home/View/HomeRootView.swift b/Trio/Sources/Modules/Home/View/HomeRootView.swift index e4ae94f97ec..d85a307a47a 100644 --- a/Trio/Sources/Modules/Home/View/HomeRootView.swift +++ b/Trio/Sources/Modules/Home/View/HomeRootView.swift @@ -1003,7 +1003,6 @@ extension Home { } .navigationTitle("Home") .navigationBarHidden(true) - .ignoresSafeArea(.keyboard) .blur(radius: state.isLoopStatusPresented ? 3 : 0) .sheet(isPresented: $state.isLoopStatusPresented) { LoopStatusView(state: state) From bfc27925645b5caaee44c29eea803294c67094c6 Mon Sep 17 00:00:00 2001 From: Sam King Date: Tue, 31 Mar 2026 10:29:51 -0700 Subject: [PATCH 62/73] Back out isSuspended change to simplify commit --- Trio/Sources/APS/APSManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Trio/Sources/APS/APSManager.swift b/Trio/Sources/APS/APSManager.swift index 63a22e32c84..c9d20513144 100644 --- a/Trio/Sources/APS/APSManager.swift +++ b/Trio/Sources/APS/APSManager.swift @@ -197,7 +197,7 @@ final class BaseAPSManager: APSManager, Injectable { .store(in: &lifetime) deviceDataManager.suspended - .receive(on: DispatchQueue.main) + .receive(on: processQueue) .sink { suspended in self.isSuspended = suspended } From 4dd90024dd8d9b5edf7f317946aed7ea3685687f Mon Sep 17 00:00:00 2001 From: Deniz Cengiz Date: Tue, 31 Mar 2026 20:19:41 +0200 Subject: [PATCH 63/73] Bump MedtrumKit; pull in i18n fixes --- MedtrumKit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MedtrumKit b/MedtrumKit index a4e8533ee9a..b7f3d44c06b 160000 --- a/MedtrumKit +++ b/MedtrumKit @@ -1 +1 @@ -Subproject commit a4e8533ee9a2ce1aa2ea80cf966189f614b44834 +Subproject commit b7f3d44c06bb7c580be897e0414e64de2d6dd995 From a8701d1bc27d71b8e883dac4b3b9c1b0003bd6d9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 31 Mar 2026 18:36:11 +0000 Subject: [PATCH 64/73] CI: Bump APP_DEV_VERSION to 0.6.0.71 [skip ci] --- Config.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Config.xcconfig b/Config.xcconfig index 3c07d69accd..5afc739a3ac 100644 --- a/Config.xcconfig +++ b/Config.xcconfig @@ -19,7 +19,7 @@ TRIO_APP_GROUP_ID = group.org.nightscout.$(DEVELOPMENT_TEAM).trio.trio-app-group // The developers set the version numbers, please leave them alone APP_VERSION = 0.6.0 -APP_DEV_VERSION = 0.6.0.70 +APP_DEV_VERSION = 0.6.0.71 APP_BUILD_NUMBER = 1 COPYRIGHT_NOTICE = From c2ec95e1f4c748397a1e2ed9b82e33ff0cc3a9e0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 31 Mar 2026 18:40:25 +0000 Subject: [PATCH 65/73] CI: Bump APP_DEV_VERSION to 0.6.0.72 [skip ci] --- Config.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Config.xcconfig b/Config.xcconfig index 5afc739a3ac..49ce8648d9f 100644 --- a/Config.xcconfig +++ b/Config.xcconfig @@ -19,7 +19,7 @@ TRIO_APP_GROUP_ID = group.org.nightscout.$(DEVELOPMENT_TEAM).trio.trio-app-group // The developers set the version numbers, please leave them alone APP_VERSION = 0.6.0 -APP_DEV_VERSION = 0.6.0.71 +APP_DEV_VERSION = 0.6.0.72 APP_BUILD_NUMBER = 1 COPYRIGHT_NOTICE = From f770bb6d7cfa351ae272fc88330355427103bace Mon Sep 17 00:00:00 2001 From: Marvin Polscheit Date: Wed, 18 Mar 2026 17:33:56 +0100 Subject: [PATCH 66/73] Fix basal profile chart not showing active rate at chart's left edge e --- .../View/Chart/ChartElements/BasalChart.swift | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/Trio/Sources/Modules/Home/View/Chart/ChartElements/BasalChart.swift b/Trio/Sources/Modules/Home/View/Chart/ChartElements/BasalChart.swift index 593c5ccb9a4..8f64666a50c 100644 --- a/Trio/Sources/Modules/Home/View/Chart/ChartElements/BasalChart.swift +++ b/Trio/Sources/Modules/Home/View/Chart/ChartElements/BasalChart.swift @@ -177,6 +177,7 @@ extension MainChartView { let startOfDay = Calendar.current.startOfDay(for: beginDate) let profile = state.basalProfile var basalPoints: [BasalProfile] = [] + var lastEntryBeforeRange: (amount: Double, date: Date)? // Iterate over the next three days, multiplying the time intervals for dayOffset in 0 ..< 3 { @@ -185,8 +186,12 @@ extension MainChartView { let basalTime = startOfDay.addingTimeInterval(entry.minutes.minutes.timeInterval + dayTimeOffset) let basalTimeInterval = basalTime.timeIntervalSince1970 - // Only append points within the timeBegin and timeEnd range - if basalTimeInterval >= timeBegin, basalTimeInterval < timeEnd { + if basalTimeInterval < timeBegin { + // Track the last profile entry before the visible range + if lastEntryBeforeRange == nil || basalTime > lastEntryBeforeRange!.date { + lastEntryBeforeRange = (amount: Double(entry.rate), date: basalTime) + } + } else if basalTimeInterval < timeEnd { basalPoints.append(BasalProfile( amount: Double(entry.rate), isOverwritten: false, @@ -196,6 +201,15 @@ extension MainChartView { } } + // Include the active profile entry at timeBegin so the line starts at the chart's left edge + if let lastBefore = lastEntryBeforeRange { + basalPoints.append(BasalProfile( + amount: lastBefore.amount, + isOverwritten: false, + startDate: beginDate + )) + } + return basalPoints } From 4752a758c1b5bb23712fbe65e70f261c97a9864b Mon Sep 17 00:00:00 2001 From: Mike Plante <82073483+MikePlante1@users.noreply.github.com> Date: Tue, 31 Mar 2026 15:36:58 -0400 Subject: [PATCH 67/73] Xcode fixems --- Trio/Resources/Info.plist | 4 ++-- TrioTests/GlucoseSmoothingTests.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Trio/Resources/Info.plist b/Trio/Resources/Info.plist index 6ceab61b7b9..7f4870503d7 100644 --- a/Trio/Resources/Info.plist +++ b/Trio/Resources/Info.plist @@ -107,8 +107,8 @@ remote-notification audio - UIDesignRequiresCompatibility - + UIDesignRequiresCompatibility + UIFileSharingEnabled UILaunchScreen diff --git a/TrioTests/GlucoseSmoothingTests.swift b/TrioTests/GlucoseSmoothingTests.swift index 79e8d995e7b..a39af08ce33 100644 --- a/TrioTests/GlucoseSmoothingTests.swift +++ b/TrioTests/GlucoseSmoothingTests.swift @@ -121,7 +121,7 @@ import Testing @Test( "Exponential smoothing stops at gaps >= 12 minutes and only updates the most recent window" - ) func testExponentialSmoothingGapStopsWindow() async throws { + ) func testExponentialSmoothingGapStopsWindow() async throws { let now = Date() var dates: [Date] = [] From 98568bc7085c6494dd67f81656b8bab326730258 Mon Sep 17 00:00:00 2001 From: Mike Plante <82073483+MikePlante1@users.noreply.github.com> Date: Tue, 31 Mar 2026 16:01:47 -0400 Subject: [PATCH 68/73] typo --- Trio/Resources/Info.plist | 2 +- Trio/Resources/InfoPlist.xcstrings | 56 +----------------------------- 2 files changed, 2 insertions(+), 56 deletions(-) diff --git a/Trio/Resources/Info.plist b/Trio/Resources/Info.plist index 7f4870503d7..36e1ead7da9 100644 --- a/Trio/Resources/Info.plist +++ b/Trio/Resources/Info.plist @@ -80,7 +80,7 @@ NSContactsUsageDescription Contact is used to create a Apple Watch complication NSFaceIDUsageDescription - For authorized acces to bolus + For authorized access to bolus NSHealthShareUsageDescription Health App is used to store blood glucose, carbs and insulin NSHealthUpdateUsageDescription diff --git a/Trio/Resources/InfoPlist.xcstrings b/Trio/Resources/InfoPlist.xcstrings index 82e1dead5a0..0d028ece1a2 100644 --- a/Trio/Resources/InfoPlist.xcstrings +++ b/Trio/Resources/InfoPlist.xcstrings @@ -616,18 +616,6 @@ "comment" : "Privacy - Face ID Usage Description", "extractionState" : "extracted_with_value", "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "For authorized acces to bolus" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "For authorized acces to bolus" - } - }, "da" : { "stringUnit" : { "state" : "translated", @@ -643,19 +631,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "For authorized acces to bolus" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "For authorized acces to bolus" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "For authorized acces to bolus" + "value" : "For authorized access to bolus" } }, "fr" : { @@ -664,18 +640,6 @@ "value" : "Pour les accès autorisés au bolus" } }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "For authorized acces to bolus" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "For authorized acces to bolus" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -694,24 +658,6 @@ "value" : "Voor geautoriseerde toegang tot bolus" } }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "For authorized acces to bolus" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "For authorized acces to bolus" - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "For authorized acces to bolus" - } - }, "ru" : { "stringUnit" : { "state" : "translated", From 553424df2205658b54ea07f0650587c9e2a6b7aa Mon Sep 17 00:00:00 2001 From: Mike Plante <82073483+MikePlante1@users.noreply.github.com> Date: Tue, 31 Mar 2026 16:31:56 -0400 Subject: [PATCH 69/73] Correct "BG reading values" to "glucose values" --- Trio/Resources/Info.plist | 2 +- Trio/Resources/InfoPlist.xcstrings | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Trio/Resources/Info.plist b/Trio/Resources/Info.plist index 36e1ead7da9..e29193b5476 100644 --- a/Trio/Resources/Info.plist +++ b/Trio/Resources/Info.plist @@ -74,7 +74,7 @@ NSBluetoothPeripheralUsageDescription Bluetooth is used to communicate with insulin pump and continuous glucose monitor devices NSCalendarsFullAccessUsageDescription - To create events with BG reading values, so that they can be viewed on Apple Watch and CarPlay + To create events with glucose values, so that they can be viewed on Apple Watch and CarPlay NSCalendarsUsageDescription Calendar is used to create a new glucose events. NSContactsUsageDescription diff --git a/Trio/Resources/InfoPlist.xcstrings b/Trio/Resources/InfoPlist.xcstrings index 0d028ece1a2..f5bf442baf2 100644 --- a/Trio/Resources/InfoPlist.xcstrings +++ b/Trio/Resources/InfoPlist.xcstrings @@ -457,6 +457,18 @@ } } }, + "NSCalendarsFullAccessUsageDescription" : { + "comment" : "Privacy - Calendars Full Access Usage Description", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "To create events with glucose values, so that they can be viewed on Apple Watch and CarPlay" + } + } + } + }, "NSCalendarsUsageDescription" : { "comment" : "Privacy - Calendars Usage Description", "extractionState" : "extracted_with_value", From fb2bcb272028f356c7b753c927825774b885158d Mon Sep 17 00:00:00 2001 From: Mike Plante <82073483+MikePlante1@users.noreply.github.com> Date: Tue, 31 Mar 2026 16:36:00 -0400 Subject: [PATCH 70/73] typo --- Trio/Resources/Info.plist | 2 +- Trio/Resources/InfoPlist.xcstrings | 56 +----------------------------- 2 files changed, 2 insertions(+), 56 deletions(-) diff --git a/Trio/Resources/Info.plist b/Trio/Resources/Info.plist index e29193b5476..cfa5a10ab2a 100644 --- a/Trio/Resources/Info.plist +++ b/Trio/Resources/Info.plist @@ -76,7 +76,7 @@ NSCalendarsFullAccessUsageDescription To create events with glucose values, so that they can be viewed on Apple Watch and CarPlay NSCalendarsUsageDescription - Calendar is used to create a new glucose events. + Calendar is used to create new glucose events. NSContactsUsageDescription Contact is used to create a Apple Watch complication NSFaceIDUsageDescription diff --git a/Trio/Resources/InfoPlist.xcstrings b/Trio/Resources/InfoPlist.xcstrings index f5bf442baf2..94e6a48adea 100644 --- a/Trio/Resources/InfoPlist.xcstrings +++ b/Trio/Resources/InfoPlist.xcstrings @@ -473,18 +473,6 @@ "comment" : "Privacy - Calendars Usage Description", "extractionState" : "extracted_with_value", "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "Calendar is used to create a new glucose events." - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Calendar is used to create a new glucose events." - } - }, "da" : { "stringUnit" : { "state" : "translated", @@ -500,19 +488,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "Calendar is used to create a new glucose events." - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Calendar is used to create a new glucose events." - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Calendar is used to create a new glucose events." + "value" : "Calendar is used to create new glucose events." } }, "fr" : { @@ -521,18 +497,6 @@ "value" : "Le calendrier est utilisé pour créer un nouvel événement de glycémie." } }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "Calendar is used to create a new glucose events." - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Calendar is used to create a new glucose events." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -551,24 +515,6 @@ "value" : "Agenda wordt gebruikt om nieuwe glucose gebeurtenissen aan te maken." } }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Calendar is used to create a new glucose events." - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Calendar is used to create a new glucose events." - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Calendar is used to create a new glucose events." - } - }, "ru" : { "stringUnit" : { "state" : "translated", From 78bfc21d26531ce67aebb6e259c4b983eefb3b52 Mon Sep 17 00:00:00 2001 From: Mike Plante <82073483+MikePlante1@users.noreply.github.com> Date: Tue, 31 Mar 2026 16:44:24 -0400 Subject: [PATCH 71/73] typo --- Trio/Resources/Info.plist | 2 +- Trio/Resources/InfoPlist.xcstrings | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Trio/Resources/Info.plist b/Trio/Resources/Info.plist index cfa5a10ab2a..7bb1d98181d 100644 --- a/Trio/Resources/Info.plist +++ b/Trio/Resources/Info.plist @@ -74,7 +74,7 @@ NSBluetoothPeripheralUsageDescription Bluetooth is used to communicate with insulin pump and continuous glucose monitor devices NSCalendarsFullAccessUsageDescription - To create events with glucose values, so that they can be viewed on Apple Watch and CarPlay + To create events with glucose values, so they can be viewed on Apple Watch and CarPlay NSCalendarsUsageDescription Calendar is used to create new glucose events. NSContactsUsageDescription diff --git a/Trio/Resources/InfoPlist.xcstrings b/Trio/Resources/InfoPlist.xcstrings index 94e6a48adea..54de55c8c41 100644 --- a/Trio/Resources/InfoPlist.xcstrings +++ b/Trio/Resources/InfoPlist.xcstrings @@ -464,7 +464,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "To create events with glucose values, so that they can be viewed on Apple Watch and CarPlay" + "value" : "To create events with glucose values, so they can be viewed on Apple Watch and CarPlay" } } } From 4bd164b5e934ca05122c4ccddca0660c290e6919 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 31 Mar 2026 21:38:34 +0000 Subject: [PATCH 72/73] CI: Bump APP_DEV_VERSION to 0.6.0.73 [skip ci] --- Config.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Config.xcconfig b/Config.xcconfig index 49ce8648d9f..ed6086ca61c 100644 --- a/Config.xcconfig +++ b/Config.xcconfig @@ -19,7 +19,7 @@ TRIO_APP_GROUP_ID = group.org.nightscout.$(DEVELOPMENT_TEAM).trio.trio-app-group // The developers set the version numbers, please leave them alone APP_VERSION = 0.6.0 -APP_DEV_VERSION = 0.6.0.72 +APP_DEV_VERSION = 0.6.0.73 APP_BUILD_NUMBER = 1 COPYRIGHT_NOTICE = From a2e6c25df6f7409edf8c1d1fe090dbb15d067117 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 31 Mar 2026 21:39:10 +0000 Subject: [PATCH 73/73] CI: Bump APP_DEV_VERSION to 0.6.0.74 [skip ci] --- Config.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Config.xcconfig b/Config.xcconfig index ed6086ca61c..be138fa618d 100644 --- a/Config.xcconfig +++ b/Config.xcconfig @@ -19,7 +19,7 @@ TRIO_APP_GROUP_ID = group.org.nightscout.$(DEVELOPMENT_TEAM).trio.trio-app-group // The developers set the version numbers, please leave them alone APP_VERSION = 0.6.0 -APP_DEV_VERSION = 0.6.0.73 +APP_DEV_VERSION = 0.6.0.74 APP_BUILD_NUMBER = 1 COPYRIGHT_NOTICE =