Просмотр исходного кода

Merge branch 'dev' of github.com:nightscout/Trio-dev into beta-fixes-3

Deniz Cengiz 1 год назад
Родитель
Сommit
6696c56f81
28 измененных файлов с 849 добавлено и 475 удалено
  1. 1 1
      DanaKit
  2. 1 1
      OmniBLE
  3. 1 1
      OmniKit
  4. 23 15
      Trio.xcodeproj/project.pbxproj
  5. 5 5
      Trio/Sources/APS/CGM/CGMType.swift
  6. 1 2
      Trio/Sources/APS/CGM/PluginSource.swift
  7. 0 8
      Trio/Sources/APS/DeviceDataManager.swift
  8. 1 0
      Trio/Sources/APS/FetchGlucoseManager.swift
  9. 15 0
      Trio/Sources/Helpers/CGMOptions.swift
  10. 4 0
      Trio/Sources/Modules/Base/BaseStateModel.swift
  11. 0 9
      Trio/Sources/Modules/CGM/CGMDataFlow.swift
  12. 0 5
      Trio/Sources/Modules/CGM/CGMProvider.swift
  13. 0 266
      Trio/Sources/Modules/CGM/View/CGMRootView.swift
  14. 5 0
      Trio/Sources/Modules/CGMSettings/CGMSettingsDataFlow.swift
  15. 5 0
      Trio/Sources/Modules/CGMSettings/CGMSettingsProvider.swift
  16. 69 65
      Trio/Sources/Modules/CGM/CGMStateModel.swift
  17. 206 0
      Trio/Sources/Modules/CGMSettings/View/CGMRootView.swift
  18. 1 1
      Trio/Sources/Modules/CGM/View/CGMSettingsView.swift
  19. 2 2
      Trio/Sources/Modules/CGM/View/CGMSetupView.swift
  20. 210 0
      Trio/Sources/Modules/CGMSettings/View/CustomCGMOptionsView.swift
  21. 26 15
      Trio/Sources/Modules/Calibrations/CalibrationsStateModel.swift
  22. 3 1
      Trio/Sources/Modules/Calibrations/View/CalibrationsRootView.swift
  23. 165 27
      Trio/Sources/Modules/Home/HomeStateModel.swift
  24. 0 17
      Trio/Sources/Modules/Home/View/Header/PumpView.swift
  25. 100 12
      Trio/Sources/Modules/Home/View/HomeRootView.swift
  26. 0 15
      Trio/Sources/Modules/NightscoutConfig/View/NightscoutConnectView.swift
  27. 4 3
      Trio/Sources/Modules/PumpConfig/View/PumpConfigRootView.swift
  28. 1 4
      Trio/Sources/Router/Screen.swift

+ 1 - 1
DanaKit

@@ -1 +1 @@
-Subproject commit b07f236677b205d31d7ecf6144970348e8d5a3fe
+Subproject commit b44c5df260a8b38d6fd0b5851cc3aac5da5d9d57

+ 1 - 1
OmniBLE

@@ -1 +1 @@
-Subproject commit 1fa2874419225c8c7af0d9afbd9faf823cda34e5
+Subproject commit 6f65cbae4c8089a892911e273204edfc4cc81e9d

+ 1 - 1
OmniKit

@@ -1 +1 @@
-Subproject commit 48a35efa52f42e0b72fe2e984f60d4482a11a75f
+Subproject commit 39915b05fe46b5d73eca52e156dd7efd11193ee8

+ 23 - 15
Trio.xcodeproj/project.pbxproj

@@ -204,7 +204,7 @@
 		38FE826D25CC8461001FF17A /* NightscoutAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FE826C25CC8461001FF17A /* NightscoutAPI.swift */; };
 		38FEF3FA2737E42000574A46 /* BaseStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF3F92737E42000574A46 /* BaseStateModel.swift */; };
 		38FEF3FC2737E53800574A46 /* MainStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF3FB2737E53800574A46 /* MainStateModel.swift */; };
-		38FEF3FE2738083E00574A46 /* CGMProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF3FD2738083E00574A46 /* CGMProvider.swift */; };
+		38FEF3FE2738083E00574A46 /* CGMSettingsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF3FD2738083E00574A46 /* CGMSettingsProvider.swift */; };
 		38FEF413273B317A00574A46 /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF412273B317A00574A46 /* HKUnit.swift */; };
 		45252C95D220E796FDB3B022 /* ConfigEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F8A87AA037BD079BA3528BA /* ConfigEditorDataFlow.swift */; };
 		45717281F743594AA9D87191 /* ConfigEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 920DDB21E5D0EB813197500D /* ConfigEditorRootView.swift */; };
@@ -286,7 +286,7 @@
 		B7C465E9472624D8A2BE2A6A /* (null) in Sources */ = {isa = PBXBuildFile; };
 		B958F1B72BA0711600484851 /* MKRingProgressView in Frameworks */ = {isa = PBXBuildFile; productRef = B958F1B62BA0711600484851 /* MKRingProgressView */; };
 		B9CAAEFC2AE70836000F68BC /* branch.txt in Resources */ = {isa = PBXBuildFile; fileRef = B9CAAEFB2AE70836000F68BC /* branch.txt */; };
-		BA00D96F7B2FF169A06FB530 /* CGMStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C018D1680307A31C9ED7120 /* CGMStateModel.swift */; };
+		BA00D96F7B2FF169A06FB530 /* CGMSettingsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C018D1680307A31C9ED7120 /* CGMSettingsStateModel.swift */; };
 		BD04ECCE2D29952A008C5FEB /* BolusProgressOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD04ECCD2D299522008C5FEB /* BolusProgressOverlay.swift */; };
 		BD0B2EF32C5998E600B3298F /* MealPresetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD0B2EF22C5998E600B3298F /* MealPresetView.swift */; };
 		BD1661312B82ADAB00256551 /* CustomProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1661302B82ADAB00256551 /* CustomProgressView.swift */; };
@@ -367,6 +367,7 @@
 		CE1F6DE72BAF1A180064EB8D /* BuildDetails.plist in Resources */ = {isa = PBXBuildFile; fileRef = CE1F6DE62BAF1A180064EB8D /* BuildDetails.plist */; };
 		CE1F6DE92BAF37C90064EB8D /* TidepoolConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1F6DE82BAF37C90064EB8D /* TidepoolConfigView.swift */; };
 		CE2FAD3A297D93F0001A872C /* BloodGlucoseExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2FAD39297D93F0001A872C /* BloodGlucoseExtensions.swift */; };
+		CE3EEF9A2D463717001944DD /* CustomCGMOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3EEF992D46370A001944DD /* CustomCGMOptionsView.swift */; };
 		CE48C86428CA69D5007C0598 /* OmniBLEPumpManagerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE48C86328CA69D5007C0598 /* OmniBLEPumpManagerExtensions.swift */; };
 		CE48C86628CA6B48007C0598 /* OmniPodManagerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE48C86528CA6B48007C0598 /* OmniPodManagerExtensions.swift */; };
 		CE51DD1C2A01970900F163F7 /* ConnectIQ 2.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE51DD1B2A01970800F163F7 /* ConnectIQ 2.xcframework */; };
@@ -414,6 +415,7 @@
 		CEE9A6592BBB418300EB5194 /* CalibrationsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE9A6542BBB418300EB5194 /* CalibrationsDataFlow.swift */; };
 		CEE9A65C2BBB41C800EB5194 /* CalibrationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE9A65B2BBB41C800EB5194 /* CalibrationService.swift */; };
 		CEE9A65E2BBC9F6500EB5194 /* CalibrationsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE9A65D2BBC9F6500EB5194 /* CalibrationsTests.swift */; };
+		CEF1ED6B2D58FB5800FAF41E /* CGMOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF1ED6A2D58FB4600FAF41E /* CGMOptions.swift */; };
 		D6D02515BBFBE64FEBE89856 /* DataTableRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 881E04BA5E0A003DE8E0A9C6 /* DataTableRootView.swift */; };
 		D6DEC113821A7F1056C4AA1E /* NightscoutConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F2A13DF0EDEEEDC4106AA2A /* NightscoutConfigDataFlow.swift */; };
 		DBA5254DBB2586C98F61220C /* ISFEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9F137F126D9F8DEB799F26 /* ISFEditorProvider.swift */; };
@@ -565,7 +567,7 @@
 		E592A3792CEEC038009A472C /* ContactImageRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E592A3712CEEC038009A472C /* ContactImageRootView.swift */; };
 		E592A37A2CEEC038009A472C /* ContactImageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E592A3742CEEC038009A472C /* ContactImageProvider.swift */; };
 		E974172296125A5AE99E634C /* PumpConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AD22C985B79A2F0D2EA3D9D /* PumpConfigRootView.swift */; };
-		F5CA3DB1F9DC8B05792BBFAA /* CGMDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B5C0607505A38F256BF99A /* CGMDataFlow.swift */; };
+		F5CA3DB1F9DC8B05792BBFAA /* CGMSettingsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B5C0607505A38F256BF99A /* CGMSettingsDataFlow.swift */; };
 		F5F7E6C1B7F098F59EB67EC5 /* TargetsEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA49538D56989D8DA6FCF538 /* TargetsEditorDataFlow.swift */; };
 		F816825E28DB441200054060 /* HeartBeatManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F816825D28DB441200054060 /* HeartBeatManager.swift */; };
 		F816826028DB441800054060 /* BluetoothTransmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F816825F28DB441800054060 /* BluetoothTransmitter.swift */; };
@@ -928,7 +930,7 @@
 		38FE826C25CC8461001FF17A /* NightscoutAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutAPI.swift; sourceTree = "<group>"; };
 		38FEF3F92737E42000574A46 /* BaseStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseStateModel.swift; sourceTree = "<group>"; };
 		38FEF3FB2737E53800574A46 /* MainStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainStateModel.swift; sourceTree = "<group>"; };
-		38FEF3FD2738083E00574A46 /* CGMProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGMProvider.swift; sourceTree = "<group>"; };
+		38FEF3FD2738083E00574A46 /* CGMSettingsProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGMSettingsProvider.swift; sourceTree = "<group>"; };
 		38FEF412273B317A00574A46 /* HKUnit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HKUnit.swift; sourceTree = "<group>"; };
 		3BDEA2DC60EDE0A3CA54DC73 /* TargetsEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorProvider.swift; sourceTree = "<group>"; };
 		3BF768BD6264FF7D71D66767 /* NightscoutConfigProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NightscoutConfigProvider.swift; sourceTree = "<group>"; };
@@ -978,7 +980,7 @@
 		5A2325512BFCBF55003518CA /* NightscoutUploadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutUploadView.swift; sourceTree = "<group>"; };
 		5A2325532BFCBF65003518CA /* NightscoutFetchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutFetchView.swift; sourceTree = "<group>"; };
 		5A2325572BFCC168003518CA /* NightscoutConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutConnectView.swift; sourceTree = "<group>"; };
-		5C018D1680307A31C9ED7120 /* CGMStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CGMStateModel.swift; sourceTree = "<group>"; };
+		5C018D1680307A31C9ED7120 /* CGMSettingsStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CGMSettingsStateModel.swift; sourceTree = "<group>"; };
 		5D5B4F8B4194BB7E260EF251 /* ConfigEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConfigEditorStateModel.swift; sourceTree = "<group>"; };
 		60744C3E9BB3652895C908CC /* DataTableProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DataTableProvider.swift; sourceTree = "<group>"; };
 		64AA5E04A2761F6EEA6568E1 /* CarbRatioEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CarbRatioEditorStateModel.swift; sourceTree = "<group>"; };
@@ -1012,7 +1014,7 @@
 		AAFF91130F2FCCC7EBBA11AD /* BasalProfileEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorStateModel.swift; sourceTree = "<group>"; };
 		AF65DA88F972B56090AD6AC3 /* PumpConfigDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpConfigDataFlow.swift; sourceTree = "<group>"; };
 		B5822B15939E719628E9FF7C /* SnoozeRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SnoozeRootView.swift; sourceTree = "<group>"; };
-		B9B5C0607505A38F256BF99A /* CGMDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CGMDataFlow.swift; sourceTree = "<group>"; };
+		B9B5C0607505A38F256BF99A /* CGMSettingsDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CGMSettingsDataFlow.swift; sourceTree = "<group>"; };
 		B9CAAEFB2AE70836000F68BC /* branch.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = branch.txt; sourceTree = SOURCE_ROOT; };
 		BA49538D56989D8DA6FCF538 /* TargetsEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorDataFlow.swift; sourceTree = "<group>"; };
 		BD04ECCD2D299522008C5FEB /* BolusProgressOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusProgressOverlay.swift; sourceTree = "<group>"; };
@@ -1097,6 +1099,7 @@
 		CE398D012977349800DF218F /* CryptoKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CryptoKit.framework; path = System/Library/Frameworks/CryptoKit.framework; sourceTree = SDKROOT; };
 		CE398D17297C9EE800DF218F /* G7SensorKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = G7SensorKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		CE398D1A297D69A900DF218F /* ShareClient.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = ShareClient.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		CE3EEF992D46370A001944DD /* CustomCGMOptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomCGMOptionsView.swift; sourceTree = "<group>"; };
 		CE48C86328CA69D5007C0598 /* OmniBLEPumpManagerExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmniBLEPumpManagerExtensions.swift; sourceTree = "<group>"; };
 		CE48C86528CA6B48007C0598 /* OmniPodManagerExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmniPodManagerExtensions.swift; sourceTree = "<group>"; };
 		CE51DD1B2A01970800F163F7 /* ConnectIQ 2.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = "ConnectIQ 2.xcframework"; path = "Dependencies/ConnectIQ 2.xcframework"; sourceTree = "<group>"; };
@@ -1144,6 +1147,7 @@
 		CEE9A6542BBB418300EB5194 /* CalibrationsDataFlow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CalibrationsDataFlow.swift; sourceTree = "<group>"; };
 		CEE9A65B2BBB41C800EB5194 /* CalibrationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalibrationService.swift; sourceTree = "<group>"; };
 		CEE9A65D2BBC9F6500EB5194 /* CalibrationsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalibrationsTests.swift; sourceTree = "<group>"; };
+		CEF1ED6A2D58FB4600FAF41E /* CGMOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGMOptions.swift; sourceTree = "<group>"; };
 		CFCFE0781F9074C2917890E8 /* ManualTempBasalStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalStateModel.swift; sourceTree = "<group>"; };
 		D0BDC6993C1087310EDFC428 /* CarbRatioEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CarbRatioEditorRootView.swift; sourceTree = "<group>"; };
 		DC2C6489D29ECCCAD78E0721 /* GlucoseNotificationSettingsStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GlucoseNotificationSettingsStateModel.swift; sourceTree = "<group>"; };
@@ -1402,6 +1406,7 @@
 		0D76BBC81CEDC1A0050F45EF /* View */ = {
 			isa = PBXGroup;
 			children = (
+				CE3EEF992D46370A001944DD /* CustomCGMOptionsView.swift */,
 				38569352270B5E350002C50D /* CGMRootView.swift */,
 				CE7950232997D81700FA576E /* CGMSettingsView.swift */,
 				CE7950252998056D00FA576E /* CGMSetupView.swift */,
@@ -1619,7 +1624,7 @@
 				DD09D4792C5986BA003FEA5D /* CalendarEventSettings */,
 				CEE9A64D2BBB411C00EB5194 /* Calibrations */,
 				E42231DBF0DBE2B4B92D1B15 /* CarbRatioEditor */,
-				F75CB57ED6971B46F8756083 /* CGM */,
+				F75CB57ED6971B46F8756083 /* CGMSettings */,
 				0610F7D6D2EC00E3BA1569F0 /* ConfigEditor */,
 				E592A3762CEEC038009A472C /* ContactImage */,
 				9E56E3626FAD933385101B76 /* DataTable */,
@@ -2119,6 +2124,7 @@
 		388E5A5A25B6F05F0019842D /* Helpers */ = {
 			isa = PBXGroup;
 			children = (
+				CEF1ED6A2D58FB4600FAF41E /* CGMOptions.swift */,
 				C2A0A42E2CE0312C003B98E8 /* ConstantValues.swift */,
 				DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */,
 				38F37827261260DC009DB701 /* Color+Extensions.swift */,
@@ -3142,15 +3148,15 @@
 			path = GlucoseNotificationSettings;
 			sourceTree = "<group>";
 		};
-		F75CB57ED6971B46F8756083 /* CGM */ = {
+		F75CB57ED6971B46F8756083 /* CGMSettings */ = {
 			isa = PBXGroup;
 			children = (
-				B9B5C0607505A38F256BF99A /* CGMDataFlow.swift */,
-				38FEF3FD2738083E00574A46 /* CGMProvider.swift */,
-				5C018D1680307A31C9ED7120 /* CGMStateModel.swift */,
+				B9B5C0607505A38F256BF99A /* CGMSettingsDataFlow.swift */,
+				38FEF3FD2738083E00574A46 /* CGMSettingsProvider.swift */,
+				5C018D1680307A31C9ED7120 /* CGMSettingsStateModel.swift */,
 				0D76BBC81CEDC1A0050F45EF /* View */,
 			);
-			path = CGM;
+			path = CGMSettings;
 			sourceTree = "<group>";
 		};
 		F90692A8274B7A980037068D /* HealthKit */ = {
@@ -3780,6 +3786,7 @@
 				DD1745522C55CA5D00211FAC /* UnitsLimitsSettingsStateModel.swift in Sources */,
 				DD2CC85C2D25DA1000445446 /* GlucoseTargetsView.swift in Sources */,
 				190EBCC429FF136900BA767D /* UserInterfaceSettingsDataFlow.swift in Sources */,
+				CE3EEF9A2D463717001944DD /* CustomCGMOptionsView.swift in Sources */,
 				5A2325582BFCC168003518CA /* NightscoutConnectView.swift in Sources */,
 				3811DEB025C9D88300A708ED /* BaseKeychain.swift in Sources */,
 				110AEDE42C5193D200615CC9 /* BolusIntentRequest.swift in Sources */,
@@ -3825,6 +3832,7 @@
 				53F2382465BF74DB1A967C8B /* PumpConfigProvider.swift in Sources */,
 				5D16287A969E64D18CE40E44 /* PumpConfigStateModel.swift in Sources */,
 				19D466AA29AA3099004D5F33 /* MealSettingsRootView.swift in Sources */,
+				CEF1ED6B2D58FB5800FAF41E /* CGMOptions.swift in Sources */,
 				E974172296125A5AE99E634C /* PumpConfigRootView.swift in Sources */,
 				DD1745502C55CA5500211FAC /* UnitsLimitsSettingsProvider.swift in Sources */,
 				581AC4392BE22ED10038760C /* JSONConverter.swift in Sources */,
@@ -3836,7 +3844,7 @@
 				E06B911A275B5EEA003C04B6 /* Array+Extension.swift in Sources */,
 				38EA0600262091870064E39B /* BolusProgressViewStyle.swift in Sources */,
 				389ECDFE2601061500D86C4F /* View+Snapshot.swift in Sources */,
-				38FEF3FE2738083E00574A46 /* CGMProvider.swift in Sources */,
+				38FEF3FE2738083E00574A46 /* CGMSettingsProvider.swift in Sources */,
 				38E98A3725F5509500C0CED0 /* String+Extensions.swift in Sources */,
 				CC76E9512BD4812E008BEB61 /* Forecast+helper.swift in Sources */,
 				DD1745242C55526000211FAC /* SMBSettingsStateModel.swift in Sources */,
@@ -3979,9 +3987,9 @@
 				D6D02515BBFBE64FEBE89856 /* DataTableRootView.swift in Sources */,
 				DD1745292C55642100211FAC /* SettingInputSection.swift in Sources */,
 				38569349270B5DFB0002C50D /* AppGroupSource.swift in Sources */,
-				F5CA3DB1F9DC8B05792BBFAA /* CGMDataFlow.swift in Sources */,
+				F5CA3DB1F9DC8B05792BBFAA /* CGMSettingsDataFlow.swift in Sources */,
 				BDF34F952C10D27300D51995 /* DeterminationData.swift in Sources */,
-				BA00D96F7B2FF169A06FB530 /* CGMStateModel.swift in Sources */,
+				BA00D96F7B2FF169A06FB530 /* CGMSettingsStateModel.swift in Sources */,
 				BD7DA9A52AE06DFC00601B20 /* BolusCalculatorConfigDataFlow.swift in Sources */,
 				6EADD581738D64431902AC0A /* (null) in Sources */,
 				CE94598729E9E4110047C9C6 /* WatchConfigRootView.swift in Sources */,

+ 5 - 5
Trio/Sources/APS/CGM/CGMType.swift

@@ -14,7 +14,7 @@ enum CGMType: String, JSON, CaseIterable, Identifiable {
         case .none:
             return "None"
         case .nightscout:
-            return "Nightscout"
+            return "Nightscout as CGM"
         case .xdrip:
             return "xDrip4iOS"
         case .simulator:
@@ -22,7 +22,7 @@ enum CGMType: String, JSON, CaseIterable, Identifiable {
         case .enlite:
             return "Medtronic Enlite"
         case .plugin:
-            return "plugin CGM"
+            return "Plugin CGM"
         }
     }
 
@@ -52,16 +52,16 @@ enum CGMType: String, JSON, CaseIterable, Identifiable {
     var subtitle: String {
         switch self {
         case .none:
-            return NSLocalizedString("None", comment: "No CGM choiced")
+            return NSLocalizedString("None", comment: "No CGM selected")
         case .nightscout:
-            return NSLocalizedString("Online or internal server", comment: "Online or internal server")
+            return NSLocalizedString("Uses your Nightscout as CGM", comment: "Online or internal server")
         case .xdrip:
             return NSLocalizedString(
                 "Using shared app group with external CGM app xDrip4iOS",
                 comment: "Shared app group xDrip4iOS"
             )
         case .simulator:
-            return NSLocalizedString("Simple simulator", comment: "Simple simulator")
+            return NSLocalizedString("Glucose Simulator for Demo Only", comment: "Simple simulator")
         case .enlite:
             return NSLocalizedString("Minilink transmitter", comment: "Minilink transmitter")
         case .plugin:

+ 1 - 2
Trio/Sources/APS/CGM/PluginSource.swift

@@ -107,8 +107,7 @@ extension PluginSource: CGMManagerDelegate {
     func cgmManagerWantsDeletion(_ manager: CGMManager) {
         dispatchPrecondition(condition: .onQueue(processQueue))
         debug(.deviceManager, " CGM Manager with identifier \(manager.pluginIdentifier) wants deletion")
-        // TODO:
-        glucoseManager?.cgmGlucoseSourceType = .none
+        glucoseManager?.deleteGlucoseSource()
     }
 
     func cgmManager(_: CGMManager, hasNew readingResult: CGMReadingResult) {

+ 0 - 8
Trio/Sources/APS/DeviceDataManager.swift

@@ -415,10 +415,6 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
             settingsManager.updateInsulinCurve(status.insulinType)
         }
 
-        broadcaster.notify(PumpTimeZoneObserver.self, on: processQueue) {
-            $0.pumpTimeZoneDidChange(status.timeZone)
-        }
-
         if let omnipod = pumpManager as? OmnipodPumpManager {
             let reservoirVal = omnipod.state.podState?.lastInsulinMeasurements?.reservoirLevel ?? 0xDEAD_BEEF
             // TODO: find the value Pod.maximumReservoirReading
@@ -700,10 +696,6 @@ protocol PumpBatteryObserver {
     func pumpBatteryDidChange(_ battery: Battery)
 }
 
-protocol PumpTimeZoneObserver {
-    func pumpTimeZoneDidChange(_ timezone: TimeZone)
-}
-
 protocol PumpDeactivatedObserver {
     func pumpDeactivatedDidChange()
 }

+ 1 - 0
Trio/Sources/APS/FetchGlucoseManager.swift

@@ -129,6 +129,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         if let manager = newManager
         {
             cgmManager = manager
+            glucoseSource = nil
             removeCalibrations()
         } else if self.cgmGlucoseSourceType == .plugin, cgmManager == nil, let rawCGMManager = rawCGMManager {
             cgmManager = cgmManagerFromRawValue(rawCGMManager)

+ 15 - 0
Trio/Sources/Helpers/CGMOptions.swift

@@ -0,0 +1,15 @@
+let cgmOptions: [CGMOption] = [
+    CGMOption(name: "Dexcom G5", predicate: { $0.type == .plugin && $0.displayName.contains("G5") }),
+    CGMOption(name: "Dexcom G6 / ONE", predicate: { $0.type == .plugin && $0.displayName.contains("G6") }),
+    CGMOption(name: "Dexcom G7 / ONE+", predicate: { $0.type == .plugin && $0.displayName.contains("G7") }),
+    CGMOption(name: "Dexcom Share", predicate: { $0.type == .plugin && $0.displayName.contains("Dexcom Share") }),
+    CGMOption(name: "FreeStyle Libre", predicate: { $0.type == .plugin && $0.displayName == "FreeStyle Libre" }),
+    CGMOption(
+        name: "FreeStyle Libre Demo",
+        predicate: { $0.type == .plugin && $0.displayName == "FreeStyle Libre Demo" }
+    ),
+    CGMOption(name: "Glucose Simulator", predicate: { $0.type == .simulator }),
+    CGMOption(name: "Medtronic Enlite", predicate: { $0.type == .enlite }),
+    CGMOption(name: "Nightscout as CGM", predicate: { $0.type == .nightscout }),
+    CGMOption(name: "xDrip4iOS", predicate: { $0.type == .xdrip })
+]

+ 4 - 0
Trio/Sources/Modules/Base/BaseStateModel.swift

@@ -11,6 +11,10 @@ protocol StateModel: ObservableObject {
     func view(for screen: Screen) -> AnyView
 }
 
+protocol CGMStateModel: StateModel {
+    var cgmCurrent: CGMType { get }
+}
+
 class BaseStateModel<Provider>: StateModel, Injectable where Provider: Trio.Provider {
     @Injected() var router: Router!
     @Injected() var settingsManager: SettingsManager!

+ 0 - 9
Trio/Sources/Modules/CGM/CGMDataFlow.swift

@@ -1,9 +0,0 @@
-enum CGM {
-    enum Config {}
-}
-
-enum cgmConfig {
-    enum Config {}
-}
-
-protocol CGMProvider: Provider {}

+ 0 - 5
Trio/Sources/Modules/CGM/CGMProvider.swift

@@ -1,5 +0,0 @@
-extension CGM {
-    final class Provider: BaseProvider, CGMProvider {
-        @Injected() var apsManager: APSManager!
-    }
-}

+ 0 - 266
Trio/Sources/Modules/CGM/View/CGMRootView.swift

@@ -1,266 +0,0 @@
-import LoopKitUI
-import SwiftUI
-import Swinject
-
-extension CGM {
-    struct RootView: BaseView {
-        let resolver: Resolver
-        let displayClose: Bool
-        @StateObject var state = StateModel()
-        @State private var setupCGM = false
-
-        @State private var shouldDisplayHint: Bool = false
-        @State var hintDetent = PresentationDetent.large
-        @State var selectedVerboseHint: AnyView?
-        @State var hintLabel: String?
-        @State private var decimalPlaceholder: Decimal = 0.0
-        @State private var booleanPlaceholder: Bool = false
-
-        @Environment(\.colorScheme) var colorScheme
-        @Environment(AppState.self) var appState
-
-        var body: some View {
-            NavigationView {
-                List {
-                    Section(
-                        header: Text("CGM Integration to Trio"),
-                        content: {
-                            VStack {
-                                Picker("Type", selection: $state.cgmCurrent) {
-                                    ForEach(state.listOfCGM) { type in
-                                        VStack(alignment: .leading) {
-                                            Text(type.displayName)
-                                            Text(type.subtitle).font(.caption).foregroundColor(.secondary)
-                                        }.tag(type)
-                                    }
-                                }.padding(.top)
-
-                                HStack(alignment: .center) {
-                                    Text(
-                                        "Select your CGM. See hint for compatible devices."
-                                    )
-                                    .font(.footnote)
-                                    .foregroundColor(.secondary)
-                                    .lineLimit(nil)
-                                    Spacer()
-                                    Button(
-                                        action: {
-                                            hintLabel = "Available CGM Types for Trio"
-                                            selectedVerboseHint =
-                                                AnyView(
-                                                    Text(
-                                                        "• Dexcom G5 \n• Dexcom G6 / ONE \n• Dexcom G7 / ONE+ \n• Dexcom Share \n• Freestyle Libre \n• Freestyle Libre Demo \n• Glucose Simulator \n• Medtronic Enlite \n• Nightscout \n• xDrip4iOS"
-                                                    )
-                                                )
-                                            shouldDisplayHint.toggle()
-                                        },
-                                        label: {
-                                            HStack {
-                                                Image(systemName: "questionmark.circle")
-                                            }
-                                        }
-                                    ).buttonStyle(BorderlessButtonStyle())
-                                }.padding(.top)
-                            }.padding(.bottom)
-
-                            if let link = state.cgmCurrent.type.externalLink {
-                                Button {
-                                    UIApplication.shared.open(link, options: [:], completionHandler: nil)
-                                } label: {
-                                    HStack {
-                                        Text("About this source")
-                                        Spacer()
-                                        Image(systemName: "chevron.right")
-                                    }
-                                }
-                                .frame(maxWidth: .infinity, alignment: .leading)
-                            }
-
-                            if state.cgmCurrent.type == .plugin {
-                                Button {
-                                    setupCGM.toggle()
-                                } label: {
-                                    HStack {
-                                        Text("CGM Configuration")
-                                        Spacer()
-                                        Image(systemName: "chevron.right")
-                                    }
-                                }
-                                .frame(maxWidth: .infinity, alignment: .leading)
-                            }
-                        }
-                    ).listRowBackground(Color.chart)
-
-                    if let appURL = state.urlOfApp() {
-                        Section {
-                            Button {
-                                UIApplication.shared.open(appURL, options: [:]) { success in
-                                    if !success {
-                                        self.router.alertMessage
-                                            .send(MessageContent(content: "Unable to open the app", type: .warning))
-                                    }
-                                }
-                            }
-
-                            label: {
-                                Label(state.displayNameOfApp() ?? "-", systemImage: "waveform.path.ecg.rectangle").font(.title3)
-                                    .padding() }
-                                .frame(maxWidth: .infinity, alignment: .center)
-                                .buttonStyle(.bordered)
-                        }
-                        .listRowBackground(Color.clear)
-                    } else if state.cgmCurrent.type == .nightscout {
-                        if let url = state.url {
-                            Section {
-                                Button {
-                                    UIApplication.shared.open(url, options: [:]) { success in
-                                        if !success {
-                                            self.router.alertMessage
-                                                .send(MessageContent(content: "No URL available", type: .warning))
-                                        }
-                                    }
-                                }
-                                label: { Label("Open URL", systemImage: "waveform.path.ecg.rectangle").font(.title3).padding() }
-                                    .frame(maxWidth: .infinity, alignment: .center)
-                                    .buttonStyle(.bordered)
-                            }
-                            .listRowBackground(Color.clear)
-                        } else {
-                            Section {
-                                Button {
-                                    state.showModal(for: .nighscoutConfigDirect)
-                                }
-                                label: {
-                                    Label("Config Nightscout", systemImage: "waveform.path.ecg.rectangle").font(.title3).padding()
-                                }
-                                .frame(maxWidth: .infinity, alignment: .center)
-                                .buttonStyle(.bordered)
-                            }
-                            .listRowBackground(Color.clear)
-                        }
-                    }
-
-                    if state.cgmCurrent.type == .xdrip {
-                        Section(header: Text("Heartbeat")) {
-                            VStack(alignment: .leading) {
-                                if let cgmTransmitterDeviceAddress = state.cgmTransmitterDeviceAddress {
-                                    Text("CGM address :").padding(.top)
-                                    Text(cgmTransmitterDeviceAddress)
-                                } else {
-                                    Text("CGM is not used as heartbeat.").padding(.top)
-                                }
-
-                                HStack(alignment: .center) {
-                                    Text(
-                                        "A heartbeat tells Trio to start a loop cycle. This is required for closed loop."
-                                    )
-                                    .font(.footnote)
-                                    .foregroundColor(.secondary)
-                                    .lineLimit(nil)
-                                    Spacer()
-                                    Button(
-                                        action: {
-                                            hintLabel = "CGM Heartbeat"
-                                            selectedVerboseHint =
-                                                AnyView(
-                                                    Text(
-                                                        "The CGM Heartbeat can come from either a CGM or a pump to wake up Trio when phone is locked or in the background. If CGM is on the same phone as Trio and xDrip4iOS is configured to use the same AppGroup as Trio and the heartbeat feature is turned on in xDrip4iOS, then the CGM can provide a heartbeat to wake up Trio when phone is locked or app is in the background."
-                                                    )
-                                                )
-                                            shouldDisplayHint.toggle()
-                                        },
-                                        label: {
-                                            HStack {
-                                                Image(systemName: "questionmark.circle")
-                                            }
-                                        }
-                                    ).buttonStyle(BorderlessButtonStyle())
-                                }.padding(.vertical)
-                            }
-                        }.listRowBackground(Color.chart)
-                    }
-
-                    if state.cgmCurrent.type == .plugin && state.cgmCurrent.id.contains("Libre") {
-                        Section {
-                            Text("Libre Calibrations").navigationLink(to: .calibrations, from: self)
-                        }.listRowBackground(Color.chart)
-                    }
-
-                    SettingInputSection(
-                        decimalValue: $decimalPlaceholder,
-                        booleanValue: $state.smoothGlucose,
-                        shouldDisplayHint: $shouldDisplayHint,
-                        selectedVerboseHint: Binding(
-                            get: { selectedVerboseHint },
-                            set: {
-                                selectedVerboseHint = $0.map { AnyView($0) }
-                                hintLabel = "Smooth Glucose Value"
-                            }
-                        ),
-                        units: state.units,
-                        type: .boolean,
-                        label: "Smooth Glucose Value",
-                        miniHint: "Smooth CGM readings using Savitzky-Golay filtering.",
-                        verboseHint:
-                        VStack(alignment: .leading, spacing: 10) {
-                            Text("Default: OFF").bold()
-                            Text(
-                                "This filter looks at small groups of nearby readings and fits them to a simple mathematical curve. This process doesn't change the overall pattern of your glucose data but helps smooth out the \"noise\" or irregular fluctuations that could lead to false highs or lows."
-                            )
-                            Text(
-                                "It's designed to keep the important trends in your data while minimizing those small, misleading variations, giving you and Trio a clearer sense of where your blood sugar is really headed. This type of filtering is useful in Trio, as it can help prevent over-corrections based on inaccurate glucose readings. This can help reduce the impact of sudden spikes or dips that might not reflect your true blood glucose levels."
-                            )
-                            Text(
-                                "Note: If enabled, the smoothed values you see in Trio may differ from what is shown in your CGM app."
-                            )
-                        }
-                    )
-                }
-                .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
-                .onAppear(perform: configureView)
-                .navigationTitle("CGM")
-                .navigationBarTitleDisplayMode(.automatic)
-                .navigationBarItems(leading: displayClose ? Button("Close", action: state.hideModal) : nil)
-                .sheet(isPresented: $shouldDisplayHint) {
-                    SettingInputHintView(
-                        hintDetent: $hintDetent,
-                        shouldDisplayHint: $shouldDisplayHint,
-                        hintLabel: hintLabel ?? "",
-                        hintText: selectedVerboseHint ?? AnyView(EmptyView()),
-                        sheetTitle: "Help"
-                    )
-                }
-                .onChange(of: setupCGM) { _, setupCGM in
-                    state.setupCGM = setupCGM
-                }
-                .onChange(of: state.setupCGM) { _, setupCGM in
-                    self.setupCGM = setupCGM
-                }
-                .screenNavigation(self)
-            }
-            .sheet(isPresented: $setupCGM) {
-                if let cgmFetchManager = state.cgmManager,
-                   let cgmManager = cgmFetchManager.cgmManager,
-                   state.cgmCurrent.type == cgmFetchManager.cgmGlucoseSourceType,
-                   state.cgmCurrent.id == cgmFetchManager.cgmGlucosePluginId
-                {
-                    CGMSettingsView(
-                        cgmManager: cgmManager,
-                        bluetoothManager: state.provider.apsManager.bluetoothManager!,
-                        unit: state.settingsManager.settings.units,
-                        completionDelegate: state
-                    )
-                } else {
-                    CGMSetupView(
-                        CGMType: state.cgmCurrent,
-                        bluetoothManager: state.provider.apsManager.bluetoothManager!,
-                        unit: state.settingsManager.settings.units,
-                        completionDelegate: state,
-                        setupDelegate: state,
-                        pluginCGMManager: self.state.pluginCGMManager
-                    )
-                }
-            }
-        }
-    }
-}

+ 5 - 0
Trio/Sources/Modules/CGMSettings/CGMSettingsDataFlow.swift

@@ -0,0 +1,5 @@
+enum CGMSettings {
+    enum Config {}
+}
+
+protocol CGMSettingsProvider: Provider {}

+ 5 - 0
Trio/Sources/Modules/CGMSettings/CGMSettingsProvider.swift

@@ -0,0 +1,5 @@
+extension CGMSettings {
+    final class Provider: BaseProvider, CGMSettingsProvider {
+        @Injected() var apsManager: APSManager!
+    }
+}

+ 69 - 65
Trio/Sources/Modules/CGM/CGMStateModel.swift

@@ -4,33 +4,60 @@ import G7SensorKit
 import LoopKitUI
 import SwiftUI
 
-struct cgmName: Identifiable, Hashable {
+struct CGMModel: Identifiable, Hashable {
     var id: String
     var type: CGMType
     var displayName: String
     var subtitle: String
 }
 
-let cgmDefaultName = cgmName(
+struct CGMOption {
+    let name: String
+    let predicate: (CGMModel) -> Bool
+}
+
+let cgmDefaultModel = CGMModel(
     id: CGMType.none.id,
     type: .none,
     displayName: CGMType.none.displayName,
     subtitle: CGMType.none.subtitle
 )
 
-extension CGM {
+struct OtherCGMSourceCompletionNotifying: CompletionNotifying {
+    var completionDelegate: (any LoopKitUI.CompletionDelegate)?
+}
+
+class CGMSetupCompletionNotifying: CompletionNotifying {
+    var completionDelegate: (any LoopKitUI.CompletionDelegate)?
+}
+
+class CGMDeletionCompletionNotifying: CompletionNotifying {
+    var completionDelegate: (any LoopKitUI.CompletionDelegate)?
+}
+
+extension CGMSettings {
     final class StateModel: BaseStateModel<Provider> {
-        @Injected() var cgmManager: FetchGlucoseManager!
+        // Singleton implementation
+        private static var _shared: StateModel?
+        static var shared: StateModel {
+            if _shared == nil {
+                _shared = StateModel()
+                _shared?.resolver = TrioApp().resolver
+            }
+            return _shared!
+        }
+
+        @Injected() var fetchGlucoseManager: FetchGlucoseManager!
         @Injected() var pluginCGMManager: PluginManager!
         @Injected() private var broadcaster: Broadcaster!
         @Injected() var nightscoutManager: NightscoutManager!
 
         @Published var units: GlucoseUnits = .mgdL
-        @Published var setupCGM: Bool = false
-        @Published var cgmCurrent = cgmDefaultName
+        @Published var shouldDisplayCGMSetupSheet: Bool = false
+        @Published var cgmCurrent = cgmDefaultModel
         @Published var smoothGlucose = false
         @Published var cgmTransmitterDeviceAddress: String? = nil
-        @Published var listOfCGM: [cgmName] = []
+        @Published var listOfCGM: [CGMModel] = []
         @Published var url: URL?
 
         override func subscribe() {
@@ -39,10 +66,10 @@ extension CGM {
             // collect the list of CGM available with plugins and CGMType defined manually
             listOfCGM = (
                 CGMType.allCases.filter { $0 != CGMType.plugin }.map {
-                    cgmName(id: $0.id, type: $0, displayName: $0.displayName, subtitle: $0.subtitle)
+                    CGMModel(id: $0.id, type: $0, displayName: $0.displayName, subtitle: $0.subtitle)
                 } +
                     pluginCGMManager.availableCGMManagers.map {
-                        cgmName(
+                        CGMModel(
                             id: $0.identifier,
                             type: CGMType.plugin,
                             displayName: $0.localizedTitle,
@@ -62,18 +89,18 @@ extension CGM {
             switch settingsManager.settings.cgm {
             case .plugin:
                 if let cgmPluginInfo = listOfCGM.first(where: { $0.id == settingsManager.settings.cgmPluginIdentifier }) {
-                    cgmCurrent = cgmName(
+                    cgmCurrent = CGMModel(
                         id: settingsManager.settings.cgmPluginIdentifier,
                         type: .plugin,
                         displayName: cgmPluginInfo.displayName,
                         subtitle: cgmPluginInfo.subtitle
                     )
                 } else {
-                    // no more type of plugin available - restart to defaut
-                    cgmCurrent = cgmDefaultName
+                    // no more type of plugin available - fallback to default model
+                    cgmCurrent = cgmDefaultModel
                 }
             default:
-                cgmCurrent = cgmName(
+                cgmCurrent = CGMModel(
                     id: settingsManager.settings.cgm.id,
                     type: settingsManager.settings.cgm,
                     displayName: settingsManager.settings.cgm.displayName,
@@ -87,77 +114,54 @@ extension CGM {
                 url = URL(string: "spikeapp://")!
             case "http://127.0.0.1:17580":
                 url = URL(string: "diabox://")!
-            //            case CGMType.libreTransmitter.appURL?.absoluteString:
-            //                showModal(for: .libreConfig)
             default: break
             }
 
             cgmTransmitterDeviceAddress = UserDefaults.standard.cgmTransmitterDeviceAddress
 
             subscribeSetting(\.smoothGlucose, on: $smoothGlucose, initial: { smoothGlucose = $0 })
-
-            $cgmCurrent
-                .removeDuplicates()
-                .sink { [weak self] value in
-                    guard let self = self else { return }
-                    guard self.cgmManager.cgmGlucoseSourceType != nil else {
-                        self.settingsManager.settings.cgm = .none
-                        return
-                    }
-                    if value.type != self.settingsManager.settings.cgm ||
-                        value.id != self.settingsManager.settings.cgmPluginIdentifier
-                    {
-                        self.settingsManager.settings.cgm = value.type
-                        self.settingsManager.settings.cgmPluginIdentifier = value.id
-                        self.cgmManager.updateGlucoseSource(
-                            cgmGlucoseSourceType: value.type,
-                            cgmGlucosePluginId: value.id
-                        )
-                        self.setupCGM = false
-                    }
-                }
-                .store(in: &lifetime)
         }
 
-        func displayNameOfApp() -> String? {
-            guard cgmManager != nil else { return nil }
-            var nameOfApp = "Open Application"
-            switch cgmManager.cgmGlucoseSourceType {
+        func addCGM(cgm: CGMModel) {
+            cgmCurrent = cgm
+            switch cgmCurrent.type {
             case .plugin:
-                nameOfApp = "Open " + (cgmManager.cgmManager?.localizedTitle ?? "Application")
+                shouldDisplayCGMSetupSheet.toggle()
             default:
-                nameOfApp = "Open " + cgmManager.cgmGlucoseSourceType.displayName
+                fetchGlucoseManager.cgmGlucoseSourceType = cgmCurrent.type
+                completionNotifyingDidComplete(OtherCGMSourceCompletionNotifying())
             }
-            return nameOfApp
         }
 
-        func urlOfApp() -> URL? {
-            guard cgmManager != nil else { return nil }
-            switch cgmManager.cgmGlucoseSourceType {
-            case .plugin:
-                return cgmManager.cgmManager?.appURL
-            default:
-                return cgmManager.cgmGlucoseSourceType.appURL
-            }
+        func deleteCGM() {
+            shouldDisplayCGMSetupSheet = false
+
+            DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: {
+                self.fetchGlucoseManager.deleteGlucoseSource()
+                self.completionNotifyingDidComplete(OtherCGMSourceCompletionNotifying())
+            })
         }
     }
 }
 
-extension CGM.StateModel: CompletionDelegate {
+extension CGMSettings.StateModel: CompletionDelegate {
     func completionNotifyingDidComplete(_: CompletionNotifying) {
-        setupCGM = false
-
         // if CGM was deleted
-        if cgmManager.cgmGlucoseSourceType == nil {
-            cgmCurrent = cgmDefaultName
-            settingsManager.settings.cgm = cgmDefaultName.type
-            settingsManager.settings.cgmPluginIdentifier = cgmDefaultName.id
-            cgmManager.deleteGlucoseSource()
+        if fetchGlucoseManager.cgmGlucoseSourceType == .none {
+            cgmCurrent = cgmDefaultModel
+            settingsManager.settings.cgm = cgmDefaultModel.type
+            settingsManager.settings.cgmPluginIdentifier = cgmDefaultModel.id
+            fetchGlucoseManager.deleteGlucoseSource()
+            shouldDisplayCGMSetupSheet = false
         } else {
-            cgmManager.updateGlucoseSource(cgmGlucoseSourceType: cgmCurrent.type, cgmGlucosePluginId: cgmCurrent.id)
+            settingsManager.settings.cgm = cgmCurrent.type
+            settingsManager.settings.cgmPluginIdentifier = cgmCurrent.id
+            fetchGlucoseManager.updateGlucoseSource(cgmGlucoseSourceType: cgmCurrent.type, cgmGlucosePluginId: cgmCurrent.id)
+            shouldDisplayCGMSetupSheet = cgmCurrent.type == .simulator || cgmCurrent.type == .nightscout || cgmCurrent
+                .type == .xdrip || cgmCurrent.type == .enlite
         }
 
-        // update if required the Glucose source
+        // update glucose source if required
         DispatchQueue.main.async {
             self.broadcaster.notify(GlucoseObserver.self, on: .main) {
                 $0.glucoseDidUpdate([])
@@ -166,10 +170,10 @@ extension CGM.StateModel: CompletionDelegate {
     }
 }
 
-extension CGM.StateModel: CGMManagerOnboardingDelegate {
+extension CGMSettings.StateModel: CGMManagerOnboardingDelegate {
     func cgmManagerOnboarding(didCreateCGMManager manager: LoopKitUI.CGMManagerUI) {
         // update the glucose source
-        cgmManager.updateGlucoseSource(
+        fetchGlucoseManager.updateGlucoseSource(
             cgmGlucoseSourceType: cgmCurrent.type,
             cgmGlucosePluginId: cgmCurrent.id,
             newManager: manager
@@ -181,7 +185,7 @@ extension CGM.StateModel: CGMManagerOnboardingDelegate {
     }
 }
 
-extension CGM.StateModel {
+extension CGMSettings.StateModel {
     func settingsDidChange(_: TrioSettings) {
         units = settingsManager.settings.units
     }

+ 206 - 0
Trio/Sources/Modules/CGMSettings/View/CGMRootView.swift

@@ -0,0 +1,206 @@
+import LoopKitUI
+import SwiftUI
+import Swinject
+
+extension CGMSettings {
+    struct RootView: BaseView {
+        let resolver: Resolver
+        let displayClose: Bool
+        @StateObject var state = StateModel()
+
+        @State private var shouldDisplayHint: Bool = false
+        @State var hintDetent = PresentationDetent.large
+        @State var selectedVerboseHint: AnyView?
+        @State var hintLabel: String?
+        @State private var decimalPlaceholder: Decimal = 0.0
+        @State private var booleanPlaceholder: Bool = false
+        @State var showCGMSelection: Bool = false
+
+        @Environment(\.colorScheme) var colorScheme
+        @Environment(AppState.self) var appState
+
+        var cgmSelectionButtons: some View {
+            ForEach(cgmOptions, id: \.name) { option in
+                if let cgm = state.listOfCGM.first(where: option.predicate) {
+                    Button(option.name) {
+                        state.addCGM(cgm: cgm)
+                    }
+                }
+            }
+        }
+
+        var body: some View {
+            NavigationView {
+                Form {
+                    Section(
+                        header: Text("CGM Integration to Trio"),
+                        content: {
+                            let cgmState = state.cgmCurrent
+                            if cgmState.type != .none {
+                                Button {
+                                    state.shouldDisplayCGMSetupSheet = true
+                                } label: {
+                                    HStack {
+                                        Image(systemName: "sensor.tag.radiowaves.forward.fill")
+                                        Text(cgmState.displayName)
+                                    }
+                                    .frame(maxWidth: .infinity, minHeight: 50, alignment: .center)
+                                    .font(.title2)
+                                }.padding()
+                            } else {
+                                VStack {
+                                    Button {
+                                        showCGMSelection.toggle()
+                                    } label: {
+                                        Text("Add CGM")
+                                            .font(.title3) }
+                                        .frame(maxWidth: .infinity, alignment: .center)
+                                        .buttonStyle(.bordered)
+
+                                    HStack(alignment: .center) {
+                                        Text(
+                                            "Pair your CGM with Trio. See hint for compatible devices."
+                                        )
+                                        .font(.footnote)
+                                        .foregroundColor(.secondary)
+                                        .lineLimit(nil)
+                                        Spacer()
+                                        Button(
+                                            action: {
+                                                shouldDisplayHint.toggle()
+                                            },
+                                            label: {
+                                                HStack {
+                                                    Image(systemName: "questionmark.circle")
+                                                }
+                                            }
+                                        ).buttonStyle(BorderlessButtonStyle())
+                                    }.padding(.top)
+                                }.padding(.vertical)
+                            }
+                        }
+                    )
+                    .listRowBackground(Color.chart)
+
+                    if state.cgmCurrent.type == .plugin && state.cgmCurrent.id.contains("Libre") {
+                        Section {
+                            NavigationLink(
+                                destination: Calibrations.RootView(resolver: resolver),
+                                label: { Text("Libre Calibrations") }
+                            )
+                        }.listRowBackground(Color.chart)
+                    }
+
+                    SettingInputSection(
+                        decimalValue: $decimalPlaceholder,
+                        booleanValue: $state.smoothGlucose,
+                        shouldDisplayHint: $shouldDisplayHint,
+                        selectedVerboseHint: Binding(
+                            get: { selectedVerboseHint },
+                            set: {
+                                selectedVerboseHint = $0.map { AnyView($0) }
+                                hintLabel = "Smooth Glucose Value"
+                            }
+                        ),
+                        units: state.units,
+                        type: .boolean,
+                        label: "Smooth Glucose Value",
+                        miniHint: "Smooth CGM readings using Savitzky-Golay filtering.",
+                        verboseHint:
+                        VStack(alignment: .leading, spacing: 10) {
+                            Text("Default: OFF").bold()
+                            Text(
+                                "This filter looks at small groups of nearby readings and fits them to a simple mathematical curve. This process doesn't change the overall pattern of your glucose data but helps smooth out the \"noise\" or irregular fluctuations that could lead to false highs or lows."
+                            )
+                            Text(
+                                "It's designed to keep the important trends in your data while minimizing those small, misleading variations, giving you and Trio a clearer sense of where your blood sugar is really headed. This type of filtering is useful in Trio, as it can help prevent over-corrections based on inaccurate glucose readings. This can help reduce the impact of sudden spikes or dips that might not reflect your true blood glucose levels."
+                            )
+                            Text(
+                                "Note: If enabled, the smoothed values you see in Trio may differ from what is shown in your CGM app."
+                            )
+                        }
+                    )
+                }
+                .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
+                .onAppear(perform: configureView)
+                .navigationTitle("CGM")
+                .navigationBarTitleDisplayMode(.automatic)
+                .navigationBarItems(leading: displayClose ? Button("Close", action: state.hideModal) : nil)
+                .sheet(isPresented: $state.shouldDisplayCGMSetupSheet) {
+                    switch state.cgmCurrent.type {
+                    case .enlite,
+                         .nightscout,
+                         .none,
+                         .simulator,
+                         .xdrip:
+
+                        CustomCGMOptionsView(
+                            resolver: self.resolver,
+                            state: state,
+                            cgmCurrent: state.cgmCurrent,
+                            deleteCGM: state.deleteCGM
+                        )
+
+                    case .plugin:
+                        if let fetchGlucoseManager = state.fetchGlucoseManager,
+                           let cgmManager = fetchGlucoseManager.cgmManager,
+                           state.cgmCurrent.type == fetchGlucoseManager.cgmGlucoseSourceType,
+                           state.cgmCurrent.id == fetchGlucoseManager.cgmGlucosePluginId
+                        {
+                            CGMSettingsView(
+                                cgmManager: cgmManager,
+                                bluetoothManager: state.provider.apsManager.bluetoothManager!,
+                                unit: state.settingsManager.settings.units,
+                                completionDelegate: state
+                            )
+                        } else {
+                            CGMSetupView(
+                                CGMType: state.cgmCurrent,
+                                bluetoothManager: state.provider.apsManager.bluetoothManager!,
+                                unit: state.settingsManager.settings.units,
+                                completionDelegate: state,
+                                setupDelegate: state,
+                                pluginCGMManager: self.state.pluginCGMManager
+                            )
+                        }
+                    }
+                }
+                .sheet(isPresented: $shouldDisplayHint) {
+                    SettingInputHintView(
+                        hintDetent: $hintDetent,
+                        shouldDisplayHint: $shouldDisplayHint,
+                        hintLabel: hintLabel ?? "",
+                        hintText: AnyView(
+                            VStack(alignment: .leading, spacing: 10) {
+                                Text(
+                                    "Current CGM Models Supported:"
+                                )
+                                VStack(alignment: .leading) {
+                                    Text("• Dexcom G5")
+                                    Text("• Dexcom G6 / ONE")
+                                    Text("• Dexcom G7 / ONE+")
+                                    Text("• Dexcom Share")
+                                    Text("• Freestyle Libre")
+                                    Text("• Freestyle Libre Demo")
+                                    Text("• Glucose Simulator")
+                                    Text("• Medtronic Enlite")
+                                    Text("• Nightscout")
+                                    Text("• xDrip4iOS")
+                                }
+                                Text(
+                                    "Note: The CGM Heartbeat can come from either a CGM or a pump to wake up Trio when phone is locked or in the background. If CGM is on the same phone as Trio and xDrip4iOS is configured to use the same AppGroup as Trio and the heartbeat feature is turned on in xDrip4iOS, then the CGM can provide a heartbeat to wake up Trio when phone is locked or app is in the background."
+                                )
+                            }
+                        ),
+                        sheetTitle: "Help"
+                    )
+                }
+                .confirmationDialog("CGM Model", isPresented: $showCGMSelection) {
+                    cgmSelectionButtons
+                } message: {
+                    Text("Select CGM Model")
+                }
+            }
+        }
+    }
+}

+ 1 - 1
Trio/Sources/Modules/CGM/View/CGMSettingsView.swift

@@ -3,7 +3,7 @@ import LoopKitUI
 import SwiftUI
 import UIKit
 
-extension CGM {
+extension CGMSettings {
     struct CGMSettingsView: UIViewControllerRepresentable {
         let cgmManager: CGMManagerUI?
         let bluetoothManager: BluetoothStateManager

+ 2 - 2
Trio/Sources/Modules/CGM/View/CGMSetupView.swift

@@ -3,9 +3,9 @@ import LoopKitUI
 import SwiftUI
 import UIKit
 
-extension CGM {
+extension CGMSettings {
     struct CGMSetupView: UIViewControllerRepresentable {
-        let CGMType: cgmName
+        let CGMType: CGMModel
         let bluetoothManager: BluetoothStateManager
         let unit: GlucoseUnits
         weak var completionDelegate: CompletionDelegate?

+ 210 - 0
Trio/Sources/Modules/CGMSettings/View/CustomCGMOptionsView.swift

@@ -0,0 +1,210 @@
+import LoopKitUI
+import SwiftUI
+import Swinject
+
+extension CGMSettings {
+    struct CustomCGMOptionsView: BaseView {
+        let resolver: Resolver
+        @ObservedObject var state: CGMSettings.StateModel
+        let cgmCurrent: CGMModel
+        let deleteCGM: () -> Void
+
+        @Environment(\.colorScheme) var colorScheme
+        @Environment(AppState.self) var appState
+        @Environment(\.presentationMode) var presentationMode
+
+        @State private var shouldDisplayDeletionConfirmation: Bool = false
+
+        var body: some View {
+            NavigationView {
+                Form {
+                    if cgmCurrent.type != .none {
+                        if cgmCurrent.type == .nightscout {
+                            nightscoutSection
+                        } else {
+                            customCGMSection
+                        }
+
+                        if let appURL = cgmCurrent.type.appURL {
+                            Section {
+                                Button {
+                                    UIApplication.shared.open(appURL, options: [:]) { success in
+                                        if !success {
+                                            self.router.alertMessage
+                                                .send(MessageContent(
+                                                    content: "Unable to open the app",
+                                                    type: .warning
+                                                ))
+                                        }
+                                    }
+                                }
+
+                                label: {
+                                    Label(
+                                        "Open \(cgmCurrent.displayName)",
+                                        systemImage: "waveform.path.ecg.rectangle"
+                                    ).font(.title3)
+                                        .padding() }
+                                    .frame(maxWidth: .infinity, alignment: .center)
+                                    .buttonStyle(.bordered)
+                            }.listRowBackground(Color.clear)
+                        }
+                    }
+                }
+                .navigationTitle(cgmCurrent.displayName)
+                .navigationBarTitleDisplayMode(.inline)
+                .toolbar {
+                    /// proper positioning should be .leading
+                    /// LoopKit submodules set placement to .trailing; we'll keep it "proper" here
+                    ToolbarItem(placement: .topBarLeading) {
+                        Button("Close") {
+                            presentationMode.wrappedValue.dismiss()
+                        }
+                    }
+                }
+                .safeAreaInset(
+                    edge: .bottom,
+                    spacing: 30
+                ) {
+                    stickyDeleteButton
+                }
+                .scrollContentBackground(.hidden)
+                .background(appState.trioBackgroundColor(for: colorScheme))
+                .confirmationDialog("Delete CGM", isPresented: $shouldDisplayDeletionConfirmation) {
+                    Button(role: .destructive) {
+                        deleteCGM()
+                    } label: {
+                        Text("Delete \(cgmCurrent.displayName)")
+                            .font(.headline)
+                            .tint(.red)
+                    }
+                } message: { Text("Are you sure you want to delete \(cgmCurrent.displayName)?") }
+            }
+        }
+
+        var nightscoutSection: some View {
+            Group {
+                Section(
+                    header: Text("Configuration"),
+                    content: {
+                        VStack(alignment: .leading, spacing: 10) {
+                            Text("CGM is not used as heartbeat.").padding(.top)
+
+                            Text(
+                                state.url == nil ?
+                                    "To configure your CGM, tap the button below. In the form that opens, enter your Nightscout credentials to connect to your instance." :
+                                    "Tap the button below to open your Nightscout instance in your iPhone's default browser."
+                            ).font(.footnote)
+                                .foregroundColor(.secondary)
+                                .lineLimit(nil)
+                                .padding(.vertical)
+                        }
+
+                        if state.url == nil {
+                            NavigationLink(
+                                destination: NightscoutConfig.RootView(resolver: resolver, displayClose: false),
+                                label: { Text("Configure Nightscout").foregroundStyle(Color.accentColor) }
+                            )
+                        }
+                    }
+                ).listRowBackground(Color.chart)
+
+                if let url = state.url {
+                    Section {
+                        Button {
+                            UIApplication.shared.open(url, options: [:]) { success in
+                                if !success {
+                                    self.router.alertMessage
+                                        .send(MessageContent(
+                                            content: "No URL available",
+                                            type: .warning
+                                        ))
+                                }
+                            }
+                        }
+                        label: {
+                            Label(
+                                "Open Nightscout",
+                                systemImage: "waveform.path.ecg.rectangle"
+                            ).font(.title3)
+                                .padding() }
+                            .frame(maxWidth: .infinity, alignment: .center)
+                            .buttonStyle(.bordered)
+                    }
+                    .listRowBackground(Color.clear)
+                }
+            }
+        }
+
+        var customCGMSection: some View {
+            Section(
+                header: Text("Configuration"),
+                content: {
+                    if cgmCurrent.type == .xdrip {
+                        VStack(alignment: .leading) {
+                            if let cgmTransmitterDeviceAddress = UserDefaults.standard
+                                .cgmTransmitterDeviceAddress
+                            {
+                                Text("CGM address :").padding(.top)
+                                Text(cgmTransmitterDeviceAddress)
+                            } else {
+                                Text("CGM is not used as heartbeat.").padding(.top)
+                            }
+
+                            HStack(alignment: .center) {
+                                Text(
+                                    "A heartbeat tells Trio to start a loop cycle. This is required for closed loop."
+                                )
+                                .font(.footnote)
+                                .foregroundColor(.secondary)
+                                .lineLimit(nil)
+                                Spacer()
+                            }.padding(.vertical)
+                        }
+                    } else if cgmCurrent.type == .simulator {
+                        Text(
+                            "Trio's glucose simulator does not offer any configuration. Its use is strictly for demonstration purposes only."
+                        )
+                    }
+
+                    if let link = cgmCurrent.type.externalLink {
+                        Button {
+                            UIApplication.shared.open(link, options: [:], completionHandler: nil)
+                        } label: {
+                            HStack {
+                                Text("About this source")
+                                Spacer()
+                                Image(systemName: "chevron.right")
+                            }
+                        }
+                        .frame(maxWidth: .infinity, alignment: .leading)
+                    }
+                }
+            ).listRowBackground(Color.chart)
+        }
+
+        var stickyDeleteButton: some View {
+            ZStack {
+                Rectangle()
+                    .frame(width: UIScreen.main.bounds.width, height: 65)
+                    .foregroundStyle(colorScheme == .dark ? Color.bgDarkerDarkBlue : Color.white)
+                    .background(.thinMaterial)
+                    .opacity(0.8)
+                    .clipShape(Rectangle())
+
+                Button(action: {
+                    shouldDisplayDeletionConfirmation.toggle()
+                }, label: {
+                    Text("Delete CGM")
+                        .frame(maxWidth: .infinity, maxHeight: .infinity)
+                        .padding(10)
+                })
+                    .frame(width: UIScreen.main.bounds.width * 0.9, height: 40, alignment: .center)
+                    .background(Color(.systemRed))
+                    .tint(.white)
+                    .clipShape(RoundedRectangle(cornerRadius: 8))
+                    .padding(5)
+            }
+        }
+    }
+}

+ 26 - 15
Trio/Sources/Modules/Calibrations/CalibrationsStateModel.swift

@@ -1,3 +1,4 @@
+import CoreData
 import Observation
 import SwiftDate
 import SwiftUI
@@ -16,7 +17,8 @@ extension Calibrations {
 
         var units: GlucoseUnits = .mgdL
 
-        private let context = CoreDataStack.shared.newTaskContext()
+        let backgroundContext = CoreDataStack.shared.newTaskContext()
+        private let viewContext = CoreDataStack.shared.persistentContainer.viewContext
 
         override func subscribe() {
             units = settingsManager.settings.units
@@ -33,21 +35,27 @@ extension Calibrations {
             }
         }
 
-        private func fetchAndProcessGlucose() -> GlucoseStored? {
-            do {
-                debugPrint("Calibrations State Model: \(#function) \(DebuggingIdentifiers.succeeded) fetched glucose")
-                return try context.fetch(GlucoseStored.fetch(
-                    NSPredicate.predicateFor20MinAgo,
-                    ascending: false,
-                    fetchLimit: 1
-                )).first
-            } catch {
-                debugPrint("Calibrations State Model: \(#function) \(DebuggingIdentifiers.failed) failed to fetch glucose")
-                return nil
+        /// - Returns: An array of NSManagedObjectIDs for glucose readings.
+        private func fetchGlucose() async -> [NSManagedObjectID] {
+            let results = await CoreDataStack.shared.fetchEntitiesAsync(
+                ofType: GlucoseStored.self,
+                onContext: backgroundContext,
+                predicate: NSPredicate.predicateFor20MinAgo,
+                key: "date",
+                ascending: false,
+                fetchLimit: 1 /// We only need the last value
+            )
+
+            return await backgroundContext.perform {
+                guard let glucoseResults = results as? [GlucoseStored] else {
+                    return []
+                }
+
+                return glucoseResults.map(\.objectID)
             }
         }
 
-        func addCalibration() {
+        @MainActor func addCalibration() async {
             defer {
                 UIApplication.shared.endEditing()
                 setupCalibrations()
@@ -58,9 +66,12 @@ extension Calibrations {
                 glucose = newCalibration.asMgdL
             }
 
-            if let lastGlucose = fetchAndProcessGlucose() {
-                let unfiltered = lastGlucose.glucose
+            let glucoseValuesIds = await fetchGlucose()
+            let glucoseObjects: [GlucoseStored] = await CoreDataStack.shared
+                .getNSManagedObject(with: glucoseValuesIds, context: viewContext)
 
+            if let lastGlucose = glucoseObjects.first {
+                let unfiltered = lastGlucose.glucose
                 let calibration = Calibration(x: Double(unfiltered), y: Double(glucose))
 
                 calibrationService.addCalibration(calibration)

+ 3 - 1
Trio/Sources/Modules/Calibrations/View/CalibrationsRootView.swift

@@ -34,7 +34,9 @@ extension Calibrations {
                             Text(state.units.rawValue).foregroundColor(.secondary)
                         }
                         Button {
-                            state.addCalibration()
+                            Task {
+                                await state.addCalibration()
+                            }
                         }
                         label: { Text("Add") }
                             .disabled(state.newCalibration <= 0)

+ 165 - 27
Trio/Sources/Modules/Home/HomeStateModel.swift

@@ -1,3 +1,4 @@
+import CGMBLEKitUI
 import Combine
 import CoreData
 import Foundation
@@ -10,6 +11,7 @@ extension Home {
     @Observable final class StateModel: BaseStateModel<Provider> {
         @ObservationIgnored @Injected() var broadcaster: Broadcaster!
         @ObservationIgnored @Injected() var apsManager: APSManager!
+        @ObservationIgnored @Injected() var pluginCGMManager: PluginManager!
         @ObservationIgnored @Injected() var fetchGlucoseManager: FetchGlucoseManager!
         @ObservationIgnored @Injected() var nightscoutManager: NightscoutManager!
         @ObservationIgnored @Injected() var determinationStorage: DeterminationStorage!
@@ -17,6 +19,11 @@ extension Home {
         @ObservationIgnored @Injected() var carbsStorage: CarbsStorage!
         @ObservationIgnored @Injected() var tempTargetStorage: TempTargetsStorage!
         @ObservationIgnored @Injected() var overrideStorage: OverrideStorage!
+
+        var cgmStateModel: CGMSettings.StateModel {
+            CGMSettings.StateModel.shared
+        }
+
         private let timer = DispatchTimer(timeInterval: 5)
         private(set) var filteredHours = 24
         var startMarker = Date(timeIntervalSinceNow: TimeInterval(hours: -24))
@@ -44,7 +51,8 @@ extension Home {
         var isExerciseModeActive: Bool = false
         var settingHalfBasalTarget: Decimal = 160
         var percentage: Int = 100
-        var setupPump = false
+        var shouldDisplayPumpSetupSheet = false
+        var shouldDisplayCGMSetupSheet = false
         var errorMessage: String?
         var errorDate: Date?
         var bolusProgress: Decimal?
@@ -64,7 +72,6 @@ extension Home {
         var displayXgridLines: Bool = false
         var displayYgridLines: Bool = false
         var thresholdLines: Bool = false
-        var timeZone: TimeZone?
         var hours: Int16 = 6
         var totalBolus: Decimal = 0
         var isLoopStatusPresented: Bool = false
@@ -91,7 +98,12 @@ extension Home {
         var isOverrideCancelled: Bool = false
         var preprocessedData: [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)] = []
         var pumpStatusHighlightMessage: String?
+        var pumpStatusBadgeImage: UIImage?
+        var pumpStatusBadgeColor: Color?
         var cgmAvailable: Bool = false
+        var listOfCGM: [CGMModel] = []
+        var cgmCurrent = cgmDefaultModel
+
         var showCarbsRequiredBadge: Bool = true
         private(set) var setupPumpType: PumpConfig.PumpType = .minimed
         var minForecast: [Int] = []
@@ -126,6 +138,10 @@ extension Home {
 
         typealias PumpEvent = PumpEventStored.EventType
 
+        override init() {
+            super.init()
+        }
+
         override func subscribe() {
             coreDataPublisher =
                 changedObjectsOnManagedObjectContextDidSavePublisher()
@@ -145,6 +161,7 @@ extension Home {
                 // We need to initialize settings and observers first
                 await self.setupSettings()
                 await self.setupPumpSettings()
+                await self.setupCGMSettings()
                 self.registerObservers()
 
                 // The rest can be initialized concurrently
@@ -180,9 +197,6 @@ extension Home {
                         self.setupReservoir()
                     }
                     group.addTask {
-                        self.setupCurrentPumpTimezone()
-                    }
-                    group.addTask {
                         self.setupOverrides()
                     }
                     group.addTask {
@@ -238,6 +252,7 @@ extension Home {
                 self.setupInsulinArray()
                 self.setupLastBolus()
                 self.displayPumpStatusHighlightMessage()
+                self.displayPumpStatusBadge()
             }.store(in: &subscriptions)
 
             coreDataPublisher?.filterByEntityName("OpenAPS_Battery").sink { [weak self] _ in
@@ -330,10 +345,11 @@ extension Home {
                         self.battery = nil
                         self.pumpName = ""
                         self.pumpExpiresAtDate = nil
-                        self.setupPump = false
+                        self.shouldDisplayPumpSetupSheet = false
                     } else {
                         self.setupReservoir()
                         self.displayPumpStatusHighlightMessage()
+                        self.displayPumpStatusBadge()
                         self.setupBatteryArray()
                     }
                 }
@@ -364,7 +380,6 @@ extension Home {
             displayYgridLines = settingsManager.settings.yGridLines
             thresholdLines = settingsManager.settings.rulerMarks
             totalInsulinDisplayType = settingsManager.settings.totalInsulinDisplayType
-            cgmAvailable = fetchGlucoseManager.cgmGlucoseSourceType != CGMType.none
             showCarbsRequiredBadge = settingsManager.settings.showCarbsRequiredBadge
             forecastDisplayType = settingsManager.settings.forecastDisplayType
             isExerciseModeActive = settingsManager.preferences.exerciseMode
@@ -374,9 +389,77 @@ extension Home {
             maxValue = settingsManager.preferences.autosensMax
         }
 
+        @MainActor private func setupCGMSettings() async {
+            cgmAvailable = fetchGlucoseManager.cgmGlucoseSourceType != CGMType.none
+
+            listOfCGM = (
+                CGMType.allCases.filter { $0 != CGMType.plugin }.map {
+                    CGMModel(id: $0.id, type: $0, displayName: $0.displayName, subtitle: $0.subtitle)
+                } +
+                    pluginCGMManager.availableCGMManagers.map {
+                        CGMModel(
+                            id: $0.identifier,
+                            type: CGMType.plugin,
+                            displayName: $0.localizedTitle,
+                            subtitle: $0.localizedTitle
+                        )
+                    }
+            ).sorted(by: { lhs, rhs in
+                if lhs.displayName == "None" {
+                    return true
+                } else if rhs.displayName == "None" {
+                    return false
+                } else {
+                    return lhs.displayName < rhs.displayName
+                }
+            })
+
+            switch settingsManager.settings.cgm {
+            case .plugin:
+                if let cgmPluginInfo = listOfCGM.first(where: { $0.id == settingsManager.settings.cgmPluginIdentifier }) {
+                    cgmCurrent = CGMModel(
+                        id: settingsManager.settings.cgmPluginIdentifier,
+                        type: .plugin,
+                        displayName: cgmPluginInfo.displayName,
+                        subtitle: cgmPluginInfo.subtitle
+                    )
+                } else {
+                    // no more type of plugin available - fallback to default
+                    cgmCurrent = cgmDefaultModel
+                }
+            default:
+                cgmCurrent = CGMModel(
+                    id: settingsManager.settings.cgm.id,
+                    type: settingsManager.settings.cgm,
+                    displayName: settingsManager.settings.cgm.displayName,
+                    subtitle: settingsManager.settings.cgm.subtitle
+                )
+            }
+        }
+
         func addPump(_ type: PumpConfig.PumpType) {
             setupPumpType = type
-            setupPump = true
+            shouldDisplayPumpSetupSheet = true
+        }
+
+        func addCGM(cgm: CGMModel) {
+            cgmCurrent = cgm
+            switch cgmCurrent.type {
+            case .plugin:
+                shouldDisplayCGMSetupSheet = true
+            default:
+                fetchGlucoseManager.cgmGlucoseSourceType = cgmCurrent.type
+                completionNotifyingDidComplete(CGMSetupCompletionNotifying())
+            }
+        }
+
+        func deleteCGM() {
+            shouldDisplayCGMSetupSheet = false
+
+            DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: {
+                self.fetchGlucoseManager?.deleteGlucoseSource()
+                self.completionNotifyingDidComplete(CGMDeletionCompletionNotifying())
+            })
         }
 
         /// Display the eventual status message provided by the manager of the pump
@@ -395,6 +478,22 @@ extension Home {
             }
         }
 
+        private func displayPumpStatusBadge(_ didDeactivate: Bool = false) {
+            DispatchQueue.main.async { [weak self] in
+                guard let self = self else { return }
+                if let statusBadge = self.provider.deviceManager.pumpManager?.pumpStatusBadge,
+                   let image = statusBadge.image, !didDeactivate
+                {
+                    pumpStatusBadgeImage = image
+                    pumpStatusBadgeColor = statusBadge.state == .critical ? .critical : .warning
+
+                } else {
+                    pumpStatusBadgeImage = nil
+                    pumpStatusBadgeColor = nil
+                }
+            }
+        }
+
         func runLoop() {
             provider.heartbeatNow()
         }
@@ -494,13 +593,6 @@ extension Home {
             }
         }
 
-        private func setupCurrentPumpTimezone() {
-            DispatchQueue.main.async { [weak self] in
-                guard let self = self else { return }
-                self.timeZone = self.provider.pumpTimeZone()
-            }
-        }
-
         private func getCurrentGlucoseTarget() async {
             let now = Date()
             let calendar = Calendar.current
@@ -547,10 +639,6 @@ extension Home {
                 }
             }
         }
-
-        func openCGM() {
-            router.mainSecondaryModalView.send(router.view(for: .cgmDirect))
-        }
     }
 }
 
@@ -562,7 +650,6 @@ extension Home.StateModel:
     BasalProfileObserver,
     BGTargetsObserver,
     PumpReservoirObserver,
-    PumpTimeZoneObserver,
     PumpDeactivatedObserver
 {
     func determinationDidUpdate(_: Determination) {
@@ -591,7 +678,11 @@ extension Home.StateModel:
         forecastDisplayType = settingsManager.settings.forecastDisplayType
         cgmAvailable = (fetchGlucoseManager.cgmGlucoseSourceType != CGMType.none)
         displayPumpStatusHighlightMessage()
+        displayPumpStatusBadge()
         setupBatteryArray()
+        Task {
+            await setupCGMSettings()
+        }
     }
 
     func preferencesDidChange(_: Preferences) {
@@ -624,21 +715,53 @@ extension Home.StateModel:
     func pumpReservoirDidChange(_: Decimal) {
         setupReservoir()
         displayPumpStatusHighlightMessage()
+        displayPumpStatusBadge()
     }
 
     func pumpDeactivatedDidChange() {
         displayPumpStatusHighlightMessage(true)
+        displayPumpStatusBadge(true)
         batteryFromPersistence = []
     }
-
-    func pumpTimeZoneDidChange(_: TimeZone) {
-        setupCurrentPumpTimezone()
-    }
 }
 
 extension Home.StateModel: CompletionDelegate {
-    func completionNotifyingDidComplete(_: CompletionNotifying) {
-        setupPump = false
+    func completionNotifyingDidComplete(_ notifying: CompletionNotifying) {
+        debug(.service, "Completion fired by: \(type(of: notifying))")
+        shouldDisplayCGMSetupSheet = false
+
+        if notifying is CGMSetupCompletionNotifying || notifying is CGMDeletionCompletionNotifying ||
+            notifying is CGMManagerSettingsNavigationViewController || notifying is any SetupTableViewControllerDelegate ||
+            notifying is any CGMManagerOnboarding
+        {
+            if fetchGlucoseManager.cgmGlucoseSourceType == .none {
+                debug(.service, "CGMDeletionCompletionNotifying: CGM Deletion Completed")
+
+                cgmCurrent = cgmDefaultModel
+                settingsManager.settings.cgm = cgmDefaultModel.type
+                settingsManager.settings.cgmPluginIdentifier = cgmDefaultModel.id
+                fetchGlucoseManager.deleteGlucoseSource()
+            } else {
+                debug(.service, "CGMSetupCompletionNotifying: CGM Setup Completed")
+
+                settingsManager.settings.cgm = cgmCurrent.type
+                settingsManager.settings.cgmPluginIdentifier = cgmCurrent.id
+                fetchGlucoseManager.updateGlucoseSource(cgmGlucoseSourceType: cgmCurrent.type, cgmGlucosePluginId: cgmCurrent.id)
+
+                shouldDisplayCGMSetupSheet = cgmCurrent.type == .simulator || cgmCurrent.type == .nightscout || cgmCurrent
+                    .type == .xdrip || cgmCurrent.type == .enlite
+            }
+
+            // update glucose source if required
+            DispatchQueue.main.async {
+                self.broadcaster.notify(GlucoseObserver.self, on: .main) {
+                    $0.glucoseDidUpdate([])
+                }
+            }
+        } else {
+            // pump related handling
+            shouldDisplayPumpSetupSheet = false // hides sheet
+        }
     }
 }
 
@@ -655,6 +778,21 @@ extension Home.StateModel: PumpManagerOnboardingDelegate {
     }
 
     func pumpManagerOnboarding(didPauseOnboarding _: PumpManagerUI) {
-        // TODO:
+        // nothing to do
+    }
+}
+
+extension Home.StateModel: CGMManagerOnboardingDelegate {
+    func cgmManagerOnboarding(didCreateCGMManager manager: LoopKitUI.CGMManagerUI) {
+        // update the glucose source
+        fetchGlucoseManager.updateGlucoseSource(
+            cgmGlucoseSourceType: cgmCurrent.type,
+            cgmGlucosePluginId: cgmCurrent.id,
+            newManager: manager
+        )
+    }
+
+    func cgmManagerOnboarding(didOnboardCGMManager _: LoopKitUI.CGMManagerUI) {
+        // nothing to do
     }
 }

+ 0 - 17
Trio/Sources/Modules/Home/View/Header/PumpView.swift

@@ -6,7 +6,6 @@ struct PumpView: View {
     let name: String
     let expiresAtDate: Date?
     let timerDate: Date
-    let timeZone: TimeZone?
     let pumpStatusHighlightMessage: String?
     let battery: [OpenAPS_Battery]
 
@@ -68,22 +67,6 @@ struct PumpView: View {
                         Capsule()
                             .stroke(reservoirColor.opacity(0.4), lineWidth: 2)
                     )
-
-                    if let timeZone = timeZone, timeZone.secondsFromGMT() != TimeZone.current.secondsFromGMT() {
-                        HStack {
-                            Image(systemName: "clock.badge.exclamationmark.fill")
-                                .font(.callout)
-                                .symbolRenderingMode(.palette)
-                                .foregroundStyle(.red, Color(.warning))
-
-                            Text("Timezone")
-                                .font(.callout)
-                                .fontWeight(.bold)
-                                .fontDesign(.rounded)
-                                .foregroundStyle(.red)
-                        }
-                        .padding(.leading, 12)
-                    }
                 }
 
                 if (battery.first?.display) != nil, let shouldBatteryDisplay = battery.first?.display, shouldBatteryDisplay {

+ 100 - 12
Trio/Sources/Modules/Home/View/HomeRootView.swift

@@ -32,6 +32,7 @@ extension Home {
         @State var showTreatments = false
         @State var selectedTab: Int = 0
         @State var showPumpSelection: Bool = false
+        @State var showCGMSelection: Bool = false
         @State var notificationsDisabled = false
         @State var timeButtons: [TimePicker] = [
             TimePicker(active: false, hours: 4),
@@ -80,6 +81,41 @@ extension Home {
             }
         }
 
+        @ViewBuilder func pumpTimezoneView(_ badgeImage: UIImage, _ badgeColor: Color) -> some View {
+            HStack {
+                Image(uiImage: badgeImage.withRenderingMode(.alwaysTemplate))
+                    .font(.system(size: 14))
+                    .colorMultiply(badgeColor)
+                Text(NSLocalizedString("Time Change Detected", comment: ""))
+                    .bold()
+                    .font(.system(size: 14))
+                    .foregroundStyle(badgeColor)
+            }
+            .onTapGesture {
+                if state.pumpDisplayState != nil {
+                    // sends user to pump settings
+                    state.shouldDisplayPumpSetupSheet.toggle()
+                }
+            }
+            .frame(maxWidth: .infinity, alignment: .center)
+            .padding(.vertical, 5)
+            .padding(.horizontal, 10)
+            .overlay(
+                Capsule()
+                    .stroke(badgeColor.opacity(0.4), lineWidth: 2)
+            )
+        }
+
+        var cgmSelectionButtons: some View {
+            ForEach(cgmOptions, id: \.name) { option in
+                if let cgm = state.listOfCGM.first(where: option.predicate) {
+                    Button(option.name) {
+                        state.addCGM(cgm: cgm)
+                    }
+                }
+            }
+        }
+
         var glucoseView: some View {
             CurrentGlucoseView(
                 timerDate: state.timerDate,
@@ -93,7 +129,11 @@ extension Home {
                 glucose: state.latestTwoGlucoseValues
             ).scaleEffect(0.9)
                 .onTapGesture {
-                    state.openCGM()
+                    if !state.cgmAvailable {
+                        showCGMSelection.toggle()
+                    } else {
+                        state.shouldDisplayCGMSetupSheet.toggle()
+                    }
                 }
                 .onLongPressGesture {
                     let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
@@ -108,7 +148,6 @@ extension Home {
                 name: state.pumpName,
                 expiresAtDate: state.pumpExpiresAtDate,
                 timerDate: state.timerDate,
-                timeZone: state.timeZone,
                 pumpStatusHighlightMessage: state.pumpStatusHighlightMessage,
                 battery: state.batteryFromPersistence
             )
@@ -118,7 +157,7 @@ extension Home {
                     showPumpSelection.toggle()
                 } else {
                     // sends user to pump settings
-                    state.setupPump.toggle()
+                    state.shouldDisplayPumpSetupSheet.toggle()
                 }
             }
         }
@@ -839,12 +878,17 @@ extension Home {
                         pumpView
                         Spacer()
                     }.padding(.leading, 20)
-                }.padding(.top, 10)
-                    .safeAreaInset(edge: .top, spacing: 0) {
-                        if notificationsDisabled {
-                            alertSafetyNotificationsView(geo: geo)
-                        }
+                }
+                .padding(.top, 10)
+                .safeAreaInset(edge: .top, spacing: 0) {
+                    if notificationsDisabled {
+                        alertSafetyNotificationsView(geo: geo)
                     }
+                    if let badgeImage = state.pumpStatusBadgeImage, let badgeColor = state.pumpStatusBadgeColor {
+                        pumpTimezoneView(badgeImage, badgeColor)
+                            .padding(.horizontal, 20)
+                    }
+                }
 
                 mealPanel(geo).padding(.top, UIDevice.adjustPadding(min: nil, max: 30))
                     .padding(.bottom, UIDevice.adjustPadding(min: nil, max: 20))
@@ -872,7 +916,7 @@ extension Home {
                         iconString: "info",
                         action: { state.isLegendPresented.toggle() }
                     )
-                }.padding([.horizontal, .top, .bottom])
+                }.padding([.horizontal, .bottom])
 
                 if let progress = state.bolusProgress {
                     bolusView(geo: geo, progress)
@@ -914,6 +958,10 @@ extension Home {
             .sheet(isPresented: $state.isLoopStatusPresented) {
                 LoopStatusView(state: state)
             }
+            .sheet(isPresented: $state.isLegendPresented) {
+                ChartLegendView(state: state)
+            }
+            // PUMP RELATED
             .confirmationDialog("Pump Model", isPresented: $showPumpSelection) {
                 Button("Medtronic") { state.addPump(.minimed) }
                 Button("Omnipod Eros") { state.addPump(.omnipod) }
@@ -921,7 +969,7 @@ extension Home {
                 Button("Dana(RS/-i)") { state.addPump(.dana) }
                 Button("Pump Simulator") { state.addPump(.simulator) }
             } message: { Text("Select Pump Model") }
-            .sheet(isPresented: $state.setupPump) {
+            .sheet(isPresented: $state.shouldDisplayPumpSetupSheet) {
                 if let pumpManager = state.provider.apsManager.pumpManager {
                     PumpConfig.PumpSettingsView(
                         pumpManager: pumpManager,
@@ -939,8 +987,48 @@ extension Home {
                     )
                 }
             }
-            .sheet(isPresented: $state.isLegendPresented) {
-                ChartLegendView(state: state)
+            // CGM RELATED
+            .confirmationDialog("CGM Model", isPresented: $showCGMSelection) {
+                cgmSelectionButtons
+            } message: {
+                Text("Select CGM Model")
+            }
+            .sheet(isPresented: $state.shouldDisplayCGMSetupSheet) {
+                switch state.cgmCurrent.type {
+                case .enlite,
+                     .nightscout,
+                     .none,
+                     .simulator,
+                     .xdrip:
+                    CGMSettings.CustomCGMOptionsView(
+                        resolver: self.resolver,
+                        state: state.cgmStateModel,
+                        cgmCurrent: state.cgmCurrent,
+                        deleteCGM: state.deleteCGM
+                    )
+                case .plugin:
+                    if let fetchGlucoseManager = state.fetchGlucoseManager,
+                       let cgmManager = fetchGlucoseManager.cgmManager,
+                       state.cgmCurrent.type == fetchGlucoseManager.cgmGlucoseSourceType,
+                       state.cgmCurrent.id == fetchGlucoseManager.cgmGlucosePluginId
+                    {
+                        CGMSettings.CGMSettingsView(
+                            cgmManager: cgmManager,
+                            bluetoothManager: state.provider.apsManager.bluetoothManager!,
+                            unit: state.settingsManager.settings.units,
+                            completionDelegate: state
+                        )
+                    } else {
+                        CGMSettings.CGMSetupView(
+                            CGMType: state.cgmCurrent,
+                            bluetoothManager: state.provider.apsManager.bluetoothManager!,
+                            unit: state.settingsManager.settings.units,
+                            completionDelegate: state,
+                            setupDelegate: state,
+                            pluginCGMManager: self.state.pluginCGMManager
+                        )
+                    }
+                }
             }
         }
 

+ 0 - 15
Trio/Sources/Modules/NightscoutConfig/View/NightscoutConnectView.swift

@@ -80,21 +80,6 @@ struct NightscoutConnectView: View {
                 }
                 .listRowBackground(Color.clear)
             }
-
-            // TODO: Find out if this is still required or needed ?!
-//            Section {
-//                Toggle("Use local glucose server", isOn: $state.useLocalSource)
-//                HStack {
-//                    Text("Port")
-//                    TextFieldWithToolBar(
-//                        text: $state.localPort,
-//                        placeholder: "",
-//                        keyboardType: .numberPad,
-//                        numberFormatter: portFormatter,
-//                        allowDecimalSeparator: false
-//                    )
-//                }
-//            } header: { Text("Local glucose source") }.listRowBackground(Color.chart)
         }
         .listSectionSpacing(sectionSpacing)
         .navigationTitle("Connect")

+ 4 - 3
Trio/Sources/Modules/PumpConfig/View/PumpConfigRootView.swift

@@ -29,10 +29,12 @@ extension PumpConfig {
                                     state.setupPump = true
                                 } label: {
                                     HStack {
-                                        Image(uiImage: pumpState.image ?? UIImage()).padding()
+                                        Image(uiImage: pumpState.image ?? UIImage())
                                         Text(pumpState.name)
                                     }
-                                }
+                                    .frame(maxWidth: .infinity, minHeight: 50, alignment: .center)
+                                    .font(.title2)
+                                }.padding()
                                 if state.alertNotAck {
                                     Spacer()
                                     Button("Acknowledge all alerts") { state.ack() }
@@ -70,7 +72,6 @@ extension PumpConfig {
                             }
                         }
                     )
-                    .padding(.top)
                     .listRowBackground(Color.chart)
                 }
                 .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))

+ 1 - 4
Trio/Sources/Router/Screen.swift

@@ -19,7 +19,6 @@ enum Screen: Identifiable, Hashable {
     case manualTempBasal
     case dataTable
     case cgm
-    case cgmDirect
     case healthkit
     case glucoseNotificationSettings
     case mealSettings
@@ -89,9 +88,7 @@ extension Screen {
         case .dataTable:
             DataTable.RootView(resolver: resolver)
         case .cgm:
-            CGM.RootView(resolver: resolver, displayClose: false)
-        case .cgmDirect:
-            CGM.RootView(resolver: resolver, displayClose: true)
+            CGMSettings.RootView(resolver: resolver, displayClose: false)
         case .healthkit:
             AppleHealthKit.RootView(resolver: resolver)
         case .glucoseNotificationSettings: