فهرست منبع

Merge branch 'dev' into max_carbs

Mike Plante 2 سال پیش
والد
کامیت
548380d44d
59فایلهای تغییر یافته به همراه1332 افزوده شده و 655 حذف شده
  1. 40 19
      .github/ISSUE_TEMPLATE/bug-report.md
  2. 1 1
      .github/ISSUE_TEMPLATE/config.yml
  3. 18 8
      .github/ISSUE_TEMPLATE/feature-request.md
  4. 20 4
      FreeAPS.xcodeproj/project.pbxproj
  5. 8 8
      FreeAPS.xcodeproj/xcshareddata/xcschemes/Trio.xcscheme
  6. 1 0
      FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json
  7. 2 2
      FreeAPS/Sources/APS/FetchGlucoseManager.swift
  8. 116 71
      FreeAPS/Sources/APS/Storage/CarbsStorage.swift
  9. 1 1
      FreeAPS/Sources/Assemblies/NetworkAssembly.swift
  10. 2 2
      FreeAPS/Sources/Localizations/Main/ar.lproj/Localizable.strings
  11. 1 1
      FreeAPS/Sources/Localizations/Main/ca.lproj/Localizable.strings
  12. 2 2
      FreeAPS/Sources/Localizations/Main/da.lproj/Localizable.strings
  13. 2 2
      FreeAPS/Sources/Localizations/Main/de.lproj/Localizable.strings
  14. 2 2
      FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings
  15. 2 2
      FreeAPS/Sources/Localizations/Main/es.lproj/Localizable.strings
  16. 2 2
      FreeAPS/Sources/Localizations/Main/fi.lproj/Localizable.strings
  17. 2 2
      FreeAPS/Sources/Localizations/Main/fr.lproj/Localizable.strings
  18. 2 2
      FreeAPS/Sources/Localizations/Main/he.lproj/Localizable.strings
  19. 2 2
      FreeAPS/Sources/Localizations/Main/hu.lproj/Localizable.strings
  20. 2 2
      FreeAPS/Sources/Localizations/Main/it.lproj/Localizable.strings
  21. 2 2
      FreeAPS/Sources/Localizations/Main/nb.lproj/Localizable.strings
  22. 2 2
      FreeAPS/Sources/Localizations/Main/nl.lproj/Localizable.strings
  23. 2 2
      FreeAPS/Sources/Localizations/Main/pl.lproj/Localizable.strings
  24. 2 2
      FreeAPS/Sources/Localizations/Main/pt-BR.lproj/Localizable.strings
  25. 2 2
      FreeAPS/Sources/Localizations/Main/pt-PT.lproj/Localizable.strings
  26. 2 2
      FreeAPS/Sources/Localizations/Main/ru.lproj/Localizable.strings
  27. 2 2
      FreeAPS/Sources/Localizations/Main/sk.lproj/Localizable.strings
  28. 2 2
      FreeAPS/Sources/Localizations/Main/sv.lproj/Localizable.strings
  29. 2 2
      FreeAPS/Sources/Localizations/Main/tr.lproj/Localizable.strings
  30. 2 2
      FreeAPS/Sources/Localizations/Main/uk.lproj/Localizable.strings
  31. 2 2
      FreeAPS/Sources/Localizations/Main/vi.lproj/Localizable.strings
  32. 3 3
      FreeAPS/Sources/Localizations/Main/zh-Hans.lproj/Localizable.strings
  33. 5 0
      FreeAPS/Sources/Models/FreeAPSSettings.swift
  34. 1 5
      FreeAPS/Sources/Models/RawFetchedProfile.swift
  35. 12 7
      FreeAPS/Sources/Modules/AddCarbs/View/AddCarbsRootView.swift
  36. 49 2
      FreeAPS/Sources/Modules/AddTempTarget/AddTempTargetStateModel.swift
  37. 199 109
      FreeAPS/Sources/Modules/AddTempTarget/View/AddTempTargetRootView.swift
  38. 5 5
      FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift
  39. 20 0
      FreeAPS/Sources/Modules/Home/HomeStateModel.swift
  40. 103 50
      FreeAPS/Sources/Modules/Home/View/Header/PumpView.swift
  41. 2 1
      FreeAPS/Sources/Modules/Home/View/HomeRootView.swift
  42. 5 1
      FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift
  43. 42 90
      FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutConfigRootView.swift
  44. 58 0
      FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutConnectView.swift
  45. 41 0
      FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutFetchView.swift
  46. 26 0
      FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutUploadView.swift
  47. 116 1
      FreeAPS/Sources/Modules/OverrideProfilesConfig/OverrideProfilesStateModel.swift
  48. 271 128
      FreeAPS/Sources/Modules/OverrideProfilesConfig/View/OverrideProfilesRootView.swift
  49. 0 2
      FreeAPS/Sources/Modules/PreferencesEditor/PreferencesEditorStateModel.swift
  50. 0 3
      FreeAPS/Sources/Modules/PreferencesEditor/View/PreferencesEditorRootView.swift
  51. 1 1
      FreeAPS/Sources/Modules/Settings/SettingsProvider.swift
  52. 4 4
      FreeAPS/Sources/Modules/Settings/SettingsStateModel.swift
  53. 3 24
      FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift
  54. 6 6
      FreeAPS/Sources/Modules/Settings/View/TidePoolConfigView.swift
  55. 46 0
      FreeAPS/Sources/Modules/Settings/View/TidepoolStartView.swift
  56. 7 3
      FreeAPS/Sources/Services/Network/NightscoutManager.swift
  57. 47 47
      FreeAPS/Sources/Services/Network/TidepoolManager.swift
  58. 9 5
      FreeAPSTests/CalibrationsTests.swift
  59. 1 1
      G7SensorKit

+ 40 - 19
.github/ISSUE_TEMPLATE/bug-report.md

@@ -1,31 +1,52 @@
 ---
 name: "\U0001F41B Bug report"
 about: Create a report to help us fix things
+title: ''
+labels: ['bug', 'needs-triage']
+assignees: ''
+projects: ['nightscout/2']
 
 ---
+## Describe the bug
+*A clear and concise description of what the bug is. Describe what you see versus what you expect to see.*
 
-**Describe the bug**
-A clear and concise description of what the bug is.
+## Attach a Log
+*Tap the Trio settings icon at the bottom of the screen, then tap 'Share logs' on the bottom of the list and attach it to this ticket.*
 
-**To Reproduce**
-Steps to reproduce the behavior:
-1. Go to '...'
-2. Click on '....'
-3. Scroll down to '....'
-4. See error
+## To Reproduce
+*Steps to reproduce the behavior:*
+1. *Go to '...'*
+2. *Click on '....'*
+3. *Scroll down to '....'*
+4. *See error*
 
-**Expected behavior**
-A clear and concise description of what you expected to happen.
+## Expected behavior
+*A clear and concise description of what you expected to happen.*
 
-**Screenshots**
-If applicable, add screenshots to help explain your problem.
+## Screenshots
+*If applicable, add screenshots to help explain your problem.*
 
-**Smartphone (please complete the following information):**
+## Setup Information (please complete the following information):
 
-**Setup Information (please complete the following information):**
-* Pump type
-* CGM type and CGM app
-* Trio version, branch and git reference (see Trio Settings)
+### Smartphone:
+* Hardware: *[e.g. iPhone 15 Pro]*
+* OS Version: *[e.g. iOS 17.5]*
 
-**Additional context**
-Add any other context about the problem here.
+### Pump:
+* Manufacturer: *[e.g. Insulet]*
+* Model: *[e.g. Omnipod Dash or Eros]*
+
+### CGM:
+* Device: *[e.g. Dexcom G7]*
+* Manager app: *[e.g. Dexcom App or xDrip4iOS]*
+
+### Trio Version:
+* Version Number: *[e.g. 1.9.2]*
+* Repo: *nightscout/trio*
+* Git Reference: *[e.g. commit hash]*
+
+## Technical Details
+*If applicable, provide any technical details that might help in diagnosing the problem. This could include logs, error messages, or relevant configuration details.*
+
+## Additional context
+*Add any other context about the problem here.*

+ 1 - 1
.github/ISSUE_TEMPLATE/config.yml

@@ -1,5 +1,5 @@
 blank_issues_enabled: false
 contact_links:
   - name: "🆘 Individual troubleshooting help: Please go to the Discord Trio Server"
-    url: https://discord.gg/fCY5svg4
+    url: https://discord.com/invite/FnwFEFUwXE
     about: Are you having an issue with your individual setup? Please first go to the Discord Trio Server and post there, with details of your setup (App version, pump, CGM, and CGM app) and the issue you are observing

+ 18 - 8
.github/ISSUE_TEMPLATE/feature-request.md

@@ -1,17 +1,27 @@
 ---
 name: "\U0001F4A1 Feature request \U0001F4A1"
 about: Suggest an idea for this project
+title: ''
+labels: ['enhancement', 'needs-triage']
+assignees: ''
+projects: ['nightscout/2']
 
 ---
 
-**Is your feature request related to a problem? Please describe.**
-A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+## Is your feature request related to a problem? Please describe.
+*Provide a clear and concise description of the problem. Explain how this issue affects your experience with the Trio app and any specific scenarios where it occurs.*
 
-**Describe the solution you'd like**
-A clear and concise description of what you want to happen.
+## Describe the solution you'd like
+*Detail the desired change or feature you'd like to see implemented in the Trio app. Be specific about how this solution would improve your experience and address the problem described above.*
 
-**Describe alternatives you've considered**
-A clear and concise description of any alternative solutions or features you've considered.
+## Describe alternatives you've considered
+*List and describe any alternative solutions or features you've considered that could also address the problem. Explain why you believe the proposed solution is the best option.*
 
-**Additional context**
-Add any other context or screenshots about the feature request here.
+## Additional context
+*Include any other context, screenshots, or relevant information that might help in understanding the issue or the proposed solution. If applicable, describe any previous discussions or decisions that relate to this feature request.*
+
+## Technical Details
+*If applicable, provide any technical details or considerations that might impact the implementation of this feature. This could include dependencies, potential risks, or required changes to existing functionalities.*
+
+## User Impact
+*(Optional) Describe the impact of this issue on your use of the Trio app. Include any specific examples or data that demonstrate how widespread or severe the problem is.*

+ 20 - 4
FreeAPS.xcodeproj/project.pbxproj

@@ -240,10 +240,14 @@
 		45717281F743594AA9D87191 /* ConfigEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 920DDB21E5D0EB813197500D /* ConfigEditorRootView.swift */; };
 		5075C1608E6249A51495C422 /* TargetsEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BDEA2DC60EDE0A3CA54DC73 /* TargetsEditorProvider.swift */; };
 		53F2382465BF74DB1A967C8B /* PumpConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8630D58BDAD6D9C650B9B39 /* PumpConfigProvider.swift */; };
+		5A2325522BFCBF55003518CA /* NightscoutUploadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A2325512BFCBF55003518CA /* NightscoutUploadView.swift */; };
+		5A2325542BFCBF66003518CA /* NightscoutFetchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A2325532BFCBF65003518CA /* NightscoutFetchView.swift */; };
+		5A2325582BFCC168003518CA /* NightscoutConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A2325572BFCC168003518CA /* NightscoutConnectView.swift */; };
 		5BFA1C2208114643B77F8CEB /* AddTempTargetProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEE53A13D26F101B332EFFC8 /* AddTempTargetProvider.swift */; };
 		5D16287A969E64D18CE40E44 /* PumpConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F60E97100041040446F44E7 /* PumpConfigStateModel.swift */; };
 		63E890B4D951EAA91C071D5C /* BasalProfileEditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFF91130F2FCCC7EBBA11AD /* BasalProfileEditorStateModel.swift */; };
 		642F76A05A4FF530463A9FD0 /* NightscoutConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8782B44544F38F2B2D82C38E /* NightscoutConfigRootView.swift */; };
+		65070A332BFDCB83006F213F /* TidepoolStartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65070A322BFDCB83006F213F /* TidepoolStartView.swift */; };
 		6632A0DC746872439A858B44 /* ISFEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BDA519C9B890FD9A5DFCF3 /* ISFEditorDataFlow.swift */; };
 		69A31254F2451C20361D172F /* BolusStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223EC0494F55A91E3EA69EF4 /* BolusStateModel.swift */; };
 		69B9A368029F7EB39F525422 /* CREditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA5E04A2761F6EEA6568E1 /* CREditorStateModel.swift */; };
@@ -290,7 +294,7 @@
 		CE1F6DD92BADF4620064EB8D /* PluginManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1F6DD82BADF4620064EB8D /* PluginManagerTests.swift */; };
 		CE1F6DDB2BAE08B60064EB8D /* TidepoolManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1F6DDA2BAE08B60064EB8D /* TidepoolManager.swift */; };
 		CE1F6DE72BAF1A180064EB8D /* BuildDetails.plist in Resources */ = {isa = PBXBuildFile; fileRef = CE1F6DE62BAF1A180064EB8D /* BuildDetails.plist */; };
-		CE1F6DE92BAF37C90064EB8D /* TidePoolConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1F6DE82BAF37C90064EB8D /* TidePoolConfigView.swift */; };
+		CE1F6DE92BAF37C90064EB8D /* TidepoolConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1F6DE82BAF37C90064EB8D /* TidepoolConfigView.swift */; };
 		CE2FAD3A297D93F0001A872C /* BloodGlucoseExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2FAD39297D93F0001A872C /* BloodGlucoseExtensions.swift */; };
 		CE48C86428CA69D5007C0598 /* OmniBLEPumpManagerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE48C86328CA69D5007C0598 /* OmniBLEPumpManagerExtensions.swift */; };
 		CE48C86628CA6B48007C0598 /* OmniPodManagerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE48C86528CA6B48007C0598 /* OmniPodManagerExtensions.swift */; };
@@ -760,6 +764,9 @@
 		44080E4709E3AE4B73054563 /* ConfigEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConfigEditorProvider.swift; sourceTree = "<group>"; };
 		4DD795BA46B193644D48138C /* TargetsEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorRootView.swift; sourceTree = "<group>"; };
 		505E09DC17A0C3D0AF4B66FE /* ISFEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ISFEditorStateModel.swift; sourceTree = "<group>"; };
+		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>"; };
 		5B8A42073A2D03A278914448 /* AddTempTargetDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddTempTargetDataFlow.swift; sourceTree = "<group>"; };
 		5C018D1680307A31C9ED7120 /* CGMStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CGMStateModel.swift; sourceTree = "<group>"; };
 		5D5B4F8B4194BB7E260EF251 /* ConfigEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConfigEditorStateModel.swift; sourceTree = "<group>"; };
@@ -767,6 +774,7 @@
 		60744C3E9BB3652895C908CC /* DataTableProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DataTableProvider.swift; sourceTree = "<group>"; };
 		618E62C9757B2F95431B5DC0 /* AddCarbsProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddCarbsProvider.swift; sourceTree = "<group>"; };
 		64AA5E04A2761F6EEA6568E1 /* CREditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CREditorStateModel.swift; sourceTree = "<group>"; };
+		65070A322BFDCB83006F213F /* TidepoolStartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TidepoolStartView.swift; sourceTree = "<group>"; };
 		67F94DD2853CF42BA4E30616 /* BasalProfileEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorDataFlow.swift; sourceTree = "<group>"; };
 		680C4420C9A345D46D90D06C /* ManualTempBasalProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalProvider.swift; sourceTree = "<group>"; };
 		6B1A8D012B14D88B00E76752 /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; };
@@ -815,7 +823,7 @@
 		CE1F6DD82BADF4620064EB8D /* PluginManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginManagerTests.swift; sourceTree = "<group>"; };
 		CE1F6DDA2BAE08B60064EB8D /* TidepoolManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TidepoolManager.swift; sourceTree = "<group>"; };
 		CE1F6DE62BAF1A180064EB8D /* BuildDetails.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = BuildDetails.plist; sourceTree = "<group>"; };
-		CE1F6DE82BAF37C90064EB8D /* TidePoolConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TidePoolConfigView.swift; sourceTree = "<group>"; };
+		CE1F6DE82BAF37C90064EB8D /* TidepoolConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TidepoolConfigView.swift; sourceTree = "<group>"; };
 		CE2FAD39297D93F0001A872C /* BloodGlucoseExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloodGlucoseExtensions.swift; sourceTree = "<group>"; };
 		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; };
@@ -1275,8 +1283,9 @@
 			isa = PBXGroup;
 			children = (
 				3811DE3C25C9D4A100A708ED /* SettingsRootView.swift */,
-				CE1F6DE82BAF37C90064EB8D /* TidePoolConfigView.swift */,
+				CE1F6DE82BAF37C90064EB8D /* TidepoolConfigView.swift */,
 				DD1DB7CD2BED00CF0048B367 /* SettingsRootViewModel.swift */,
+				65070A322BFDCB83006F213F /* TidepoolStartView.swift */,
 			);
 			path = View;
 			sourceTree = "<group>";
@@ -1865,6 +1874,9 @@
 			isa = PBXGroup;
 			children = (
 				8782B44544F38F2B2D82C38E /* NightscoutConfigRootView.swift */,
+				5A2325512BFCBF55003518CA /* NightscoutUploadView.swift */,
+				5A2325532BFCBF65003518CA /* NightscoutFetchView.swift */,
+				5A2325572BFCC168003518CA /* NightscoutConnectView.swift */,
 			);
 			path = View;
 			sourceTree = "<group>";
@@ -2598,6 +2610,7 @@
 				CEE9A6552BBB418300EB5194 /* CalibrationsProvider.swift in Sources */,
 				19F95FF529F10FCF00314DDC /* StatProvider.swift in Sources */,
 				38F3B2EF25ED8E2A005C48AA /* TempTargetsStorage.swift in Sources */,
+				5A2325542BFCBF66003518CA /* NightscoutFetchView.swift in Sources */,
 				19B0EF2128F6D66200069496 /* Statistics.swift in Sources */,
 				3811DF1025CAAAE200A708ED /* APSManager.swift in Sources */,
 				3870FF4725EC187A0088248F /* BloodGlucose.swift in Sources */,
@@ -2648,6 +2661,7 @@
 				3811DE1825C9D40400A708ED /* Router.swift in Sources */,
 				CE7950262998056D00FA576E /* CGMSetupView.swift in Sources */,
 				38A0363B25ECF07E00FCBB52 /* GlucoseStorage.swift in Sources */,
+				65070A332BFDCB83006F213F /* TidepoolStartView.swift in Sources */,
 				190EBCC629FF138000BA767D /* StatConfigProvider.swift in Sources */,
 				38E98A2725F52C9300C0CED0 /* CollectionIssueReporter.swift in Sources */,
 				E00EEC0427368630002FF094 /* SecurityAssembly.swift in Sources */,
@@ -2661,7 +2675,7 @@
 				38569347270B5DFB0002C50D /* CGMType.swift in Sources */,
 				3821ED4C25DD18BA00BC42AD /* Constants.swift in Sources */,
 				384E803425C385E60086DB71 /* JavaScriptWorker.swift in Sources */,
-				CE1F6DE92BAF37C90064EB8D /* TidePoolConfigView.swift in Sources */,
+				CE1F6DE92BAF37C90064EB8D /* TidepoolConfigView.swift in Sources */,
 				3811DE5D25C9D4D500A708ED /* Publisher.swift in Sources */,
 				E00EEC0727368630002FF094 /* APSAssembly.swift in Sources */,
 				38B4F3AF25E2979F00E76A18 /* IndexedCollection.swift in Sources */,
@@ -2716,6 +2730,7 @@
 				38E989DD25F5021400C0CED0 /* PumpStatus.swift in Sources */,
 				38E98A2525F52C9300C0CED0 /* IssueReporter.swift in Sources */,
 				190EBCC429FF136900BA767D /* StatConfigDataFlow.swift in Sources */,
+				5A2325582BFCC168003518CA /* NightscoutConnectView.swift in Sources */,
 				3811DEB025C9D88300A708ED /* BaseKeychain.swift in Sources */,
 				3811DE4325C9D4A100A708ED /* SettingsProvider.swift in Sources */,
 				45252C95D220E796FDB3B022 /* ConfigEditorDataFlow.swift in Sources */,
@@ -2860,6 +2875,7 @@
 				BA00D96F7B2FF169A06FB530 /* CGMStateModel.swift in Sources */,
 				CE94598729E9E4110047C9C6 /* WatchConfigRootView.swift in Sources */,
 				19E1F7E829D082D0005C8D20 /* IconConfigDataFlow.swift in Sources */,
+				5A2325522BFCBF55003518CA /* NightscoutUploadView.swift in Sources */,
 				E3A08AAE59538BC8A8ABE477 /* NotificationsConfigDataFlow.swift in Sources */,
 				1956FB212AFF79E200C7B4FF /* CoreDataStorage.swift in Sources */,
 				0F7A65FBD2CD8D6477ED4539 /* NotificationsConfigProvider.swift in Sources */,

+ 8 - 8
FreeAPS.xcodeproj/xcshareddata/xcschemes/Trio.xcscheme

@@ -263,7 +263,7 @@
             </BuildableReference>
          </TestableReference>
          <TestableReference
-            skipped = "YES">
+            skipped = "NO">
             <BuildableReference
                BuildableIdentifier = "primary"
                BlueprintIdentifier = "43CABDFC1C3506F100005705"
@@ -273,7 +273,7 @@
             </BuildableReference>
          </TestableReference>
          <TestableReference
-            skipped = "YES">
+            skipped = "NO">
             <BuildableReference
                BuildableIdentifier = "primary"
                BlueprintIdentifier = "C17F50CD291EAC3800555EB5"
@@ -283,7 +283,7 @@
             </BuildableReference>
          </TestableReference>
          <TestableReference
-            skipped = "YES">
+            skipped = "NO">
             <BuildableReference
                BuildableIdentifier = "primary"
                BlueprintIdentifier = "43D8FDD41C728FDF0073BE78"
@@ -293,7 +293,7 @@
             </BuildableReference>
          </TestableReference>
          <TestableReference
-            skipped = "YES">
+            skipped = "NO">
             <BuildableReference
                BuildableIdentifier = "primary"
                BlueprintIdentifier = "B4CEE2DF257129780093111B"
@@ -303,7 +303,7 @@
             </BuildableReference>
          </TestableReference>
          <TestableReference
-            skipped = "YES">
+            skipped = "NO">
             <BuildableReference
                BuildableIdentifier = "primary"
                BlueprintIdentifier = "C13CC34029C7B73A007F25DE"
@@ -313,7 +313,7 @@
             </BuildableReference>
          </TestableReference>
          <TestableReference
-            skipped = "YES">
+            skipped = "NO">
             <BuildableReference
                BuildableIdentifier = "primary"
                BlueprintIdentifier = "84752E8A26ED0FFE009FD801"
@@ -323,7 +323,7 @@
             </BuildableReference>
          </TestableReference>
          <TestableReference
-            skipped = "YES">
+            skipped = "NO">
             <BuildableReference
                BuildableIdentifier = "primary"
                BlueprintIdentifier = "C12ED9C929C7DBA900435701"
@@ -333,7 +333,7 @@
             </BuildableReference>
          </TestableReference>
          <TestableReference
-            skipped = "YES">
+            skipped = "NO">
             <BuildableReference
                BuildableIdentifier = "primary"
                BlueprintIdentifier = "431CE7761F98564200255374"

+ 1 - 0
FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json

@@ -5,6 +5,7 @@
   "useAutotune" : false,
   "onlyAutotuneBasals" : false,
   "isUploadEnabled" : false,
+  "isDownloadEnabled" : false,
   "useLocalGlucoseSource" : false,
   "localGlucosePort" : 8080,
   "debugOptions" : false,

+ 2 - 2
FreeAPS/Sources/APS/FetchGlucoseManager.swift

@@ -30,7 +30,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
     private let processQueue = DispatchQueue(label: "BaseGlucoseManager.processQueue")
     @Injected() var glucoseStorage: GlucoseStorage!
     @Injected() var nightscoutManager: NightscoutManager!
-    @Injected() var tidePoolService: TidePoolManager!
+    @Injected() var tidepoolService: TidepoolManager!
     @Injected() var apsManager: APSManager!
     @Injected() var settingsManager: SettingsManager!
     @Injected() var healthKitManager: HealthKitManager!
@@ -235,7 +235,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         deviceDataManager.heartbeat(date: Date())
 
         nightscoutManager.uploadGlucose()
-        tidePoolService.uploadGlucose(device: cgmManager?.cgmManagerStatus.device)
+        tidepoolService.uploadGlucose(device: cgmManager?.cgmManagerStatus.device)
 
         let glucoseForHealth = filteredByDate.filter { !glucoseFromHealth.contains($0) }
 

+ 116 - 71
FreeAPS/Sources/APS/Storage/CarbsStorage.swift

@@ -27,93 +27,53 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         injectServices(resolver)
     }
 
+    /**
+     Processes and stores carbohydrate entries, including handling entries with fat and protein to calculate and distribute future carb equivalents.
+
+     - The function processes fat and protein units (FPUs) by creating carb equivalents for future use.
+     - Ensures each carb equivalent is at least 1.0 grams by adjusting the interval if necessary.
+     - Stores the actual carbohydrate entries.
+     - Saves the data to CoreData for statistical purposes.
+     - Notifies observers of the carbohydrate data update.
+
+     - Parameters:
+       - entries: An array of `CarbsEntry` objects representing the carbohydrate entries to be processed and stored.
+     */
     func storeCarbs(_ entries: [CarbsEntry]) {
         processQueue.sync {
             let file = OpenAPS.Monitor.carbHistory
-            var uniqEvents: [CarbsEntry] = []
-
-            let fat = entries.last?.fat ?? 0
-            let protein = entries.last?.protein ?? 0
-
-            if fat > 0 || protein > 0 {
-                // -------------------------- FPU--------------------------------------
-                let interval = settings.settings.minuteInterval // Interval betwwen carbs
-                let timeCap = settings.settings.timeCap // Max Duration
-                let adjustment = settings.settings.individualAdjustmentFactor
-                let delay = settings.settings.delay // Tme before first future carb entry
-                let kcal = protein * 4 + fat * 9
-                let carbEquivalents = (kcal / 10) * adjustment
-                let fpus = carbEquivalents / 10
-                // Duration in hours used for extended boluses with Warsaw Method. Here used for total duration of the computed carbquivalents instead, excluding the configurable delay.
-                var computedDuration = 0
-                switch fpus {
-                case ..<2:
-                    computedDuration = 3
-                case 2 ..< 3:
-                    computedDuration = 4
-                case 3 ..< 4:
-                    computedDuration = 5
-                default:
-                    computedDuration = timeCap
-                }
-                // Size of each created carb equivalent if 60 minutes interval
-                var equivalent: Decimal = carbEquivalents / Decimal(computedDuration)
-                // Adjust for interval setting other than 60 minutes
-                equivalent /= Decimal(60 / interval)
-                // Round to 1 fraction digit
-                // equivalent = Decimal(round(Double(equivalent * 10) / 10))
-                let roundedEquivalent: Double = round(Double(equivalent * 10)) / 10
-                equivalent = Decimal(roundedEquivalent)
-                // Number of equivalents
-                var numberOfEquivalents = carbEquivalents / equivalent
-                // Only use delay in first loop
-                var firstIndex = true
-                // New date for each carb equivalent
-                var useDate = entries.last?.createdAt ?? Date()
-                // Group and Identify all FPUs together
-                let fpuID = UUID().uuidString
-                // Create an array of all future carb equivalents.
-                var futureCarbArray = [CarbsEntry]()
-                while carbEquivalents > 0, numberOfEquivalents > 0 {
-                    if firstIndex {
-                        useDate = useDate.addingTimeInterval(delay.minutes.timeInterval)
-                        firstIndex = false
-                    } else { useDate = useDate.addingTimeInterval(interval.minutes.timeInterval) }
-
-                    let eachCarbEntry = CarbsEntry(
-                        id: UUID().uuidString, createdAt: useDate,
-                        carbs: equivalent, fat: 0, protein: 0, note: nil,
-                        enteredBy: CarbsEntry.manual, isFPU: true,
-                        fpuID: fpuID
-                    )
-                    futureCarbArray.append(eachCarbEntry)
-                    numberOfEquivalents -= 1
-                }
-                // Save the array
+            var entriesToStore: [CarbsEntry] = []
+
+            guard let lastEntry = entries.last else { return }
+
+            if let fat = lastEntry.fat, let protein = lastEntry.protein, fat > 0 || protein > 0 {
+                let (futureCarbArray, carbEquivalents) = processFPU(
+                    entries: entries,
+                    fat: fat,
+                    protein: protein,
+                    createdAt: lastEntry.createdAt
+                )
                 if carbEquivalents > 0 {
                     self.storage.transaction { storage in
                         storage.append(futureCarbArray, to: file, uniqBy: \.id)
-                        uniqEvents = storage.retrieve(file, as: [CarbsEntry].self)?
+                        entriesToStore = storage.retrieve(file, as: [CarbsEntry].self)?
                             .filter { $0.createdAt.addingTimeInterval(1.days.timeInterval) > Date() }
                             .sorted { $0.createdAt > $1.createdAt } ?? []
-                        storage.save(Array(uniqEvents), as: file)
+                        storage.save(Array(entriesToStore), as: file)
                     }
                 }
-            } // ------------------------- END OF TPU ----------------------------------------
-            // Store the actual (normal) carbs
-            if entries.last?.carbs ?? 0 > 0 {
-                uniqEvents = []
+            }
+
+            if lastEntry.carbs > 0 {
                 self.storage.transaction { storage in
                     storage.append(entries, to: file, uniqBy: \.createdAt)
-                    uniqEvents = storage.retrieve(file, as: [CarbsEntry].self)?
+                    entriesToStore = storage.retrieve(file, as: [CarbsEntry].self)?
                         .filter { $0.createdAt.addingTimeInterval(1.days.timeInterval) > Date() }
                         .sorted { $0.createdAt > $1.createdAt } ?? []
-                    storage.save(Array(uniqEvents), as: file)
+                    storage.save(Array(entriesToStore), as: file)
                 }
             }
 
-            // MARK: Save to CoreData. TEST
-
             var cbs: Decimal = 0
             var carbDate = Date()
             if entries.isNotEmpty {
@@ -131,11 +91,96 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
                 }
             }
             broadcaster.notify(CarbsObserver.self, on: processQueue) {
-                $0.carbsDidUpdate(uniqEvents)
+                $0.carbsDidUpdate(entriesToStore)
             }
         }
     }
 
+    /**
+     Calculates the duration for processing FPUs (fat and protein units) based on the FPUs and the time cap.
+
+     - The function uses predefined rules to determine the duration based on the number of FPUs.
+     - Ensures that the duration does not exceed the time cap.
+
+     - Parameters:
+       - fpus: The number of FPUs calculated from fat and protein.
+       - timeCap: The maximum allowed duration.
+
+     - Returns: The computed duration in hours.
+     */
+    private func calculateComputedDuration(fpus: Decimal, timeCap: Int) -> Int {
+        switch fpus {
+        case ..<2:
+            return 3
+        case 2 ..< 3:
+            return 4
+        case 3 ..< 4:
+            return 5
+        default:
+            return timeCap
+        }
+    }
+
+    /**
+     Processes fat and protein entries to generate future carb equivalents, ensuring each equivalent is at least 1.0 grams.
+
+     - The function calculates the equivalent carb dosage size and adjusts the interval to ensure each equivalent is at least 1.0 grams.
+     - Creates future carb entries based on the adjusted carb equivalent size and interval.
+
+     - Parameters:
+       - entries: An array of `CarbsEntry` objects representing the carbohydrate entries to be processed.
+       - fat: The amount of fat in the last entry.
+       - protein: The amount of protein in the last entry.
+       - createdAt: The creation date of the last entry.
+
+     - Returns: A tuple containing the array of future carb entries and the total carb equivalents.
+     */
+    private func processFPU(entries _: [CarbsEntry], fat: Decimal, protein: Decimal, createdAt: Date) -> ([CarbsEntry], Decimal) {
+        let interval = settings.settings.minuteInterval
+        let timeCap = settings.settings.timeCap
+        let adjustment = settings.settings.individualAdjustmentFactor
+        let delay = settings.settings.delay
+
+        let kcal = protein * 4 + fat * 9
+        let carbEquivalents = (kcal / 10) * adjustment
+        let fpus = carbEquivalents / 10
+        var computedDuration = calculateComputedDuration(fpus: fpus, timeCap: timeCap)
+
+        var carbEquivalentSize: Decimal = carbEquivalents / Decimal(computedDuration)
+        carbEquivalentSize /= Decimal(60 / interval)
+
+        if carbEquivalentSize < 1.0 {
+            carbEquivalentSize = 1.0
+            computedDuration = Int(carbEquivalents / carbEquivalentSize)
+        }
+
+        let roundedEquivalent: Double = round(Double(carbEquivalentSize * 10)) / 10
+        carbEquivalentSize = Decimal(roundedEquivalent)
+        var numberOfEquivalents = carbEquivalents / carbEquivalentSize
+
+        var useDate = createdAt
+        let fpuID = UUID().uuidString
+        var futureCarbArray = [CarbsEntry]()
+        var firstIndex = true
+
+        while carbEquivalents > 0, numberOfEquivalents > 0 {
+            useDate = firstIndex ? useDate.addingTimeInterval(delay.minutes.timeInterval) : useDate
+                .addingTimeInterval(interval.minutes.timeInterval)
+            firstIndex = false
+
+            let eachCarbEntry = CarbsEntry(
+                id: UUID().uuidString, createdAt: useDate,
+                carbs: carbEquivalentSize, fat: 0, protein: 0, note: nil,
+                enteredBy: CarbsEntry.manual, isFPU: true,
+                fpuID: fpuID
+            )
+            futureCarbArray.append(eachCarbEntry)
+            numberOfEquivalents -= 1
+        }
+
+        return (futureCarbArray, carbEquivalents)
+    }
+
     func syncDate() -> Date {
         Date().addingTimeInterval(-1.days.timeInterval)
     }

+ 1 - 1
FreeAPS/Sources/Assemblies/NetworkAssembly.swift

@@ -8,6 +8,6 @@ final class NetworkAssembly: Assembly {
         }
 
         container.register(NightscoutManager.self) { r in BaseNightscoutManager(resolver: r) }
-        container.register(TidePoolManager.self) { r in BaseTidePoolManager(resolver: r) }
+        container.register(TidepoolManager.self) { r in BaseTidepoolManager(resolver: r) }
     }
 }

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 2 - 2
FreeAPS/Sources/Localizations/Main/ar.lproj/Localizable.strings


+ 1 - 1
FreeAPS/Sources/Localizations/Main/ca.lproj/Localizable.strings

@@ -312,7 +312,7 @@ Enact a temp Basal or a temp target */
 "Skip Bolus screen after carbs" = "Skip Bolus screen after carbs";
 
 /* Allow remote control from NS */
-"Remote control" = "Remote control";
+"Remote Control" = "Remote Control";
 
 /* Add Medtronic pump */
 "Add Medtronic" = "Add Medtronic";

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 2 - 2
FreeAPS/Sources/Localizations/Main/da.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 2 - 2
FreeAPS/Sources/Localizations/Main/de.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 2 - 2
FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 2 - 2
FreeAPS/Sources/Localizations/Main/es.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 2 - 2
FreeAPS/Sources/Localizations/Main/fi.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 2 - 2
FreeAPS/Sources/Localizations/Main/fr.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 2 - 2
FreeAPS/Sources/Localizations/Main/he.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 2 - 2
FreeAPS/Sources/Localizations/Main/hu.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 2 - 2
FreeAPS/Sources/Localizations/Main/it.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 2 - 2
FreeAPS/Sources/Localizations/Main/nb.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 2 - 2
FreeAPS/Sources/Localizations/Main/nl.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 2 - 2
FreeAPS/Sources/Localizations/Main/pl.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 2 - 2
FreeAPS/Sources/Localizations/Main/pt-BR.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 2 - 2
FreeAPS/Sources/Localizations/Main/pt-PT.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 2 - 2
FreeAPS/Sources/Localizations/Main/ru.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 2 - 2
FreeAPS/Sources/Localizations/Main/sk.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 2 - 2
FreeAPS/Sources/Localizations/Main/sv.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 2 - 2
FreeAPS/Sources/Localizations/Main/tr.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 2 - 2
FreeAPS/Sources/Localizations/Main/uk.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 2 - 2
FreeAPS/Sources/Localizations/Main/vi.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 3 - 3
FreeAPS/Sources/Localizations/Main/zh-Hans.lproj/Localizable.strings


+ 5 - 0
FreeAPS/Sources/Models/FreeAPSSettings.swift

@@ -6,6 +6,7 @@ struct FreeAPSSettings: JSON, Equatable {
     var allowAnnouncements: Bool = false
     var useAutotune: Bool = false
     var isUploadEnabled: Bool = false
+    var isDownloadEnabled: Bool = false
     var useLocalGlucoseSource: Bool = false
     var localGlucosePort: Int = 8080
     var debugOptions: Bool = false
@@ -75,6 +76,10 @@ extension FreeAPSSettings: Decodable {
             settings.isUploadEnabled = isUploadEnabled
         }
 
+        if let isDownloadEnabled = try? container.decode(Bool.self, forKey: .isDownloadEnabled) {
+            settings.isDownloadEnabled = isDownloadEnabled
+        }
+
         if let useLocalGlucoseSource = try? container.decode(Bool.self, forKey: .useLocalGlucoseSource) {
             settings.useLocalGlucoseSource = useLocalGlucoseSource
         }

+ 1 - 5
FreeAPS/Sources/Models/RawFetchedProfile.swift

@@ -4,16 +4,12 @@ struct FetchedNightscoutProfileStore: JSON {
     let _id: String
     let defaultProfile: String
     let startDate: String
-    let mills: Decimal
     let enteredBy: String
-    let store: [String: ScheduledNightscoutProfile]
-    let created_at: String
+    let store: [String: FetchedNightscoutProfile]
 }
 
 struct FetchedNightscoutProfile: JSON {
     let dia: Decimal
-    let carbs_hr: Int
-    let delay: Decimal
     let timezone: String
     let target_low: [NightscoutTimevalue]
     let target_high: [NightscoutTimevalue]

+ 12 - 7
FreeAPS/Sources/Modules/AddCarbs/View/AddCarbsRootView.swift

@@ -8,7 +8,8 @@ extension AddCarbs {
         @StateObject var state = StateModel()
         @State var dish: String = ""
         @State var isPromtPresented = false
-        @State var saved = false
+        @State var noteSaved = false
+        @State var mealSaved = false
         @State private var showAlert = false
         @FocusState private var isFocused: Bool
 
@@ -117,10 +118,14 @@ extension AddCarbs {
                 }
 
                 Section {
-                    Button { state.add() }
+                    Button {
+                        mealSaved = true
+                        state.add()
+                    }
                     label: { Text(state.saveButtonText()).font(.title3) }
                         .disabled(
-                            state.carbs > state.maxCarbs
+                            mealSaved
+                                || state.carbs > state.maxCarbs
                                 || state.fat > state.maxFat
                                 || state.protein > state.maxProtein
                                 || (state.carbs <= 0 && state.fat <= 0 && state.protein <= 0)
@@ -148,8 +153,8 @@ extension AddCarbs {
                 Section {
                     TextField("Name Of Dish", text: $dish)
                     Button {
-                        saved = true
-                        if dish != "", saved {
+                        noteSaved = true
+                        if dish != "", noteSaved {
                             let preset = Presets(context: moc)
                             preset.dish = dish
                             preset.fat = state.fat as NSDecimalNumber
@@ -157,14 +162,14 @@ extension AddCarbs {
                             preset.carbs = state.carbs as NSDecimalNumber
                             try? moc.save()
                             state.addNewPresetToWaitersNotepad(dish)
-                            saved = false
+                            noteSaved = false
                             isPromtPresented = false
                         }
                     }
                     label: { Text("Save") }
                     Button {
                         dish = ""
-                        saved = false
+                        noteSaved = false
                         isPromtPresented = false }
                     label: { Text("Cancel") }
                 } header: { Text("Enter Meal Preset Name") }

+ 49 - 2
FreeAPS/Sources/Modules/AddTempTarget/AddTempTargetStateModel.swift

@@ -9,7 +9,6 @@ extension AddTempTarget {
         let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
 
         @Published var low: Decimal = 0
-        // @Published var target: Decimal = 0
         @Published var high: Decimal = 0
         @Published var duration: Decimal = 0
         @Published var date = Date()
@@ -93,6 +92,14 @@ extension AddTempTarget {
             }
         }
 
+        private func convertAndRound(_ value: Decimal) -> Decimal {
+            if units == .mmolL {
+                return Decimal(round(Double(value.asMgdL)))
+            } else {
+                return Decimal(round(Double(value)))
+            }
+        }
+
         func save() {
             guard duration > 0 else {
                 return
@@ -158,7 +165,6 @@ extension AddTempTarget {
                         saveToCoreData.active = true
                         saveToCoreData.date = Date()
                         saveToCoreData.hbt = whichID?.hbt ?? 160
-                        // saveToCoreData.id = id
                         saveToCoreData.startDate = Date()
                         saveToCoreData.duration = whichID?.duration ?? 0
 
@@ -189,5 +195,46 @@ extension AddTempTarget {
             }
             return Decimal(Double(target))
         }
+
+        func computePercentage(target: Decimal) -> Decimal {
+            let c = Decimal(hbt - 100)
+            var ratio = c / (c + target - 100)
+
+            if ratio > maxValue {
+                ratio = maxValue
+            }
+
+            let adjustedPercentage = ratio * 100
+            let roundedPercentage = (adjustedPercentage as NSDecimalNumber).rounding(accordingToBehavior: nil)
+            return roundedPercentage as Decimal
+        }
+
+        func updatePreset(_ preset: TempTarget) {
+            var lowTarget = low
+
+            if viewPercantage {
+                lowTarget = Decimal(round(Double(computeTarget())))
+            }
+
+            if units == .mmolL, !viewPercantage {
+                lowTarget = Decimal(round(Double(lowTarget.asMgdL)))
+            }
+
+            let updatedPreset = TempTarget(
+                id: preset.id,
+                name: newPresetName.isEmpty ? preset.name : newPresetName,
+                createdAt: preset.createdAt,
+                targetTop: lowTarget,
+                targetBottom: lowTarget,
+                duration: duration,
+                enteredBy: preset.enteredBy,
+                reason: newPresetName.isEmpty ? preset.reason : newPresetName
+            )
+
+            if let index = presets.firstIndex(where: { $0.id == preset.id }) {
+                presets[index] = updatedPreset
+                storage.storePresets(presets)
+            }
+        }
     }
 }

+ 199 - 109
FreeAPS/Sources/Modules/AddTempTarget/View/AddTempTargetRootView.swift

@@ -10,6 +10,8 @@ extension AddTempTarget {
         @State private var isRemoveAlertPresented = false
         @State private var removeAlert: Alert?
         @State private var isEditing = false
+        @State private var selectedPreset: TempTarget?
+        @State private var isEditSheetPresented = false
 
         @FetchRequest(
             entity: TempTargetsSlider.entity(),
@@ -23,104 +25,73 @@ extension AddTempTarget {
             return formatter
         }
 
+        private var displayString: String {
+            guard let preset = selectedPreset else { return "" }
+            var low = preset.targetBottom
+            var high = preset.targetBottom // change to only use targetBottom instead of targetTop
+            if state.units == .mmolL {
+                low = low?.asMmolL
+                high = high?.asMmolL
+            }
+
+            let formattedLow = low.flatMap { formatter.string(from: $0 as NSNumber) } ?? ""
+            let formattedDuration = formatter.string(from: preset.duration as NSNumber) ?? ""
+
+            return "\(formattedLow) \(state.units.rawValue) for \(formattedDuration) min"
+        }
+
         var body: some View {
             Form {
                 if !state.presets.isEmpty {
                     Section(header: Text("Presets")) {
                         ForEach(state.presets) { preset in
                             presetView(for: preset)
-                        }
-                    }
-                }
-
-                HStack {
-                    Text("Experimental")
-                    Toggle(isOn: $state.viewPercantage) {}.controlSize(.mini)
-                    Image(systemName: "figure.highintensity.intervaltraining")
-                    Image(systemName: "fork.knife")
-                }
-
-                if state.viewPercantage {
-                    Section(
-                        header: Text("")
-                    ) {
-                        VStack {
-                            Slider(
-                                value: $state.percentage,
-                                in: 15 ...
-                                    min(Double(state.maxValue * 100), 200),
-                                step: 1,
-                                onEditingChanged: { editing in
-                                    isEditing = editing
-                                }
-                            )
-                            HStack {
-                                Text("\(state.percentage.formatted(.number)) % Insulin")
-                                    .foregroundColor(isEditing ? .orange : .blue)
-                                    .font(.largeTitle)
-                            }
-                            // Only display target slider when not 100 %
-                            if state.percentage != 100 {
-                                Divider()
-
-                                Slider(
-                                    value: $state.hbt,
-                                    in: 101 ... 295,
-                                    step: 1
-                                ).accentColor(.green)
-
-                                HStack {
-                                    Text(
-                                        (
-                                            state
-                                                .units == .mmolL ?
-                                                "\(state.computeTarget().asMmolL.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))) mmol/L" :
-                                                "\(state.computeTarget().formatted(.number.grouping(.never).rounded().precision(.fractionLength(0)))) mg/dl"
+                                .swipeActions {
+                                    Button(role: .none, action: {
+                                        removeAlert = Alert(
+                                            title: Text("Are you sure?"),
+                                            message: Text("Delete preset \n\(preset.displayName)?"),
+                                            primaryButton: .destructive(Text("Delete"), action: {
+                                                state.removePreset(id: preset.id)
+                                                isRemoveAlertPresented = false
+                                            }),
+                                            secondaryButton: .cancel()
                                         )
-                                            + NSLocalizedString("  Target Glucose", comment: "")
-                                    )
-                                    .foregroundColor(.green)
+                                        isRemoveAlertPresented = true
+                                    }) {
+                                        Label("Delete", systemImage: "trash")
+                                    }.tint(.red)
+                                    Button {
+                                        selectedPreset = preset
+                                        state.newPresetName = preset.displayName
+                                        state.low = state.units == .mmolL ? preset.targetBottom?.asMmolL ?? 0 : preset
+                                            .targetBottom ?? 0
+                                        state.duration = preset.duration
+                                        state.date = preset.date as? Date ?? Date()
+                                        isEditSheetPresented = true
+                                    } label: {
+                                        Label("Edit", systemImage: "square.and.pencil")
+                                    }
+                                    .tint(.blue)
+                                }
+                                .alert(isPresented: $isRemoveAlertPresented) {
+                                    removeAlert!
                                 }
-                            }
-                        }
-                    }
-                } else {
-                    Section(header: Text("Custom")) {
-                        HStack {
-                            Text("Target")
-                            Spacer()
-                            DecimalTextField("0", value: $state.low, formatter: formatter, cleanInput: true)
-                            Text(state.units.rawValue).foregroundColor(.secondary)
-                        }
-                        HStack {
-                            Text("Duration")
-                            Spacer()
-                            DecimalTextField("0", value: $state.duration, formatter: formatter, cleanInput: true)
-                            Text("minutes").foregroundColor(.secondary)
-                        }
-                        DatePicker("Date", selection: $state.date)
-                        Button { isPromtPresented = true }
-                        label: { Text("Save as preset") }
-                    }
-                }
-                if state.viewPercantage {
-                    Section {
-                        HStack {
-                            Text("Duration")
-                            Spacer()
-                            DecimalTextField("0", value: $state.duration, formatter: formatter, cleanInput: true)
-                            Text("minutes").foregroundColor(.secondary)
                         }
-                        DatePicker("Date", selection: $state.date)
-                        Button { isPromtPresented = true }
-                        label: { Text("Save as preset") }
-                            .disabled(state.duration == 0)
                     }
                 }
 
+                settingsSection(header: "Custom")
+
+                DatePicker("Date", selection: $state.date)
+                Button { isPromtPresented = true }
+                label: { Text("Save as preset") }
+                    .disabled(state.duration == 0)
+
                 Section {
                     Button { state.enact() }
                     label: { Text("Enact") }
+                        .disabled(state.duration == 0)
                     Button { state.cancel() }
                     label: { Text("Cancel Temp Target") }
                 }
@@ -129,6 +100,8 @@ extension AddTempTarget {
                 Form {
                     Section(header: Text("Enter preset name")) {
                         TextField("Name", text: $state.newPresetName)
+                    }
+                    Section {
                         Button {
                             state.save()
                             isPromtPresented = false
@@ -139,6 +112,10 @@ extension AddTempTarget {
                     }
                 }
             }
+            .sheet(isPresented: $isEditSheetPresented) {
+                editPresetPopover()
+                    .padding()
+            }
             .onAppear {
                 configureView()
                 state.hbt = isEnabledArray.first?.hbt ?? 160
@@ -148,13 +125,141 @@ extension AddTempTarget {
             .navigationBarItems(leading: Button("Close", action: state.hideModal))
         }
 
+        @ViewBuilder func settingsSection(header: String) -> some View {
+            HStack {
+                Text("Experimental")
+                Toggle(isOn: $state.viewPercantage) {}
+                    .controlSize(.mini)
+                    .onChange(of: state.viewPercantage) { newValue in
+                        if newValue {
+                            guard let selectedPreset = selectedPreset,
+                                  let targetBottom = selectedPreset.targetBottom else { return }
+                            let computedPercentage = state.computePercentage(target: targetBottom)
+                            state.percentage = Double(truncating: computedPercentage as NSNumber)
+                        }
+                    }
+                Image(systemName: "figure.highintensity.intervaltraining")
+                Image(systemName: "fork.knife")
+            }
+
+            if state.viewPercantage {
+                Section {
+                    VStack {
+                        Text("\(state.percentage.formatted(.number)) % Insulin")
+                            .foregroundColor(isEditing ? .orange : .blue)
+                            .font(.largeTitle)
+                            .padding(.vertical)
+                        Slider(
+                            value: $state.percentage,
+                            in: 15 ...
+                                min(Double(state.maxValue * 100), 200),
+                            step: 1,
+                            onEditingChanged: { editing in
+                                isEditing = editing
+                            }
+                        )
+                        HStack {}
+                        // Only display target slider when not 100 %
+                        if state.percentage != 100 {
+                            Spacer()
+                            Divider()
+                            Text(
+                                (
+                                    state
+                                        .units == .mmolL ?
+                                        "\(state.computeTarget().asMmolL.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))) mmol/L" :
+                                        "\(state.computeTarget().formatted(.number.grouping(.never).rounded().precision(.fractionLength(0)))) mg/dl"
+                                )
+                                    + NSLocalizedString(" Target Glucose", comment: "")
+                            )
+                            .foregroundColor(.green)
+                            .padding(.vertical)
+                            Slider(
+                                value: $state.hbt,
+                                in: 101 ... 295,
+                                step: 1
+                            ).accentColor(.green)
+                        }
+                    }
+                }
+            } else {
+                Section(header: Text(header)) {
+                    HStack {
+                        Text("Target")
+                        Spacer()
+                        DecimalTextField("0", value: $state.low, formatter: formatter, cleanInput: true)
+                        Text(state.units.rawValue).foregroundColor(.secondary)
+                    }
+                    HStack {
+                        Text("Duration")
+                        Spacer()
+                        DecimalTextField("0", value: $state.duration, formatter: formatter, cleanInput: true)
+                        Text("minutes").foregroundColor(.secondary)
+                    }
+                }
+            }
+            if state.viewPercantage {
+                Section {
+                    HStack {
+                        Text("Duration")
+                        Spacer()
+                        DecimalTextField("0", value: $state.duration, formatter: formatter, cleanInput: true)
+                        Text("minutes").foregroundColor(.secondary)
+                    }
+                }
+            }
+        }
+
+        @ViewBuilder private func editPresetPopover() -> some View {
+            Form {
+                Section(header: Text("Edit Name?")) {
+                    TextField("Name", text: $state.newPresetName)
+                    Text("Settings before change: \(displayString)")
+                        .foregroundColor(.secondary)
+                        .font(.caption)
+                }
+                settingsSection(header: "New target and duration")
+
+                Section {
+                    Button("Save") {
+                        guard let selectedPreset = selectedPreset else { return }
+                        state.updatePreset(selectedPreset)
+                        isEditSheetPresented = false
+                    }
+                    .disabled(state.newPresetName.isEmpty)
+
+                    Button("Cancel") {
+                        // Reset the fields and close the sheet
+                        resetFields()
+                        isEditSheetPresented = false
+                    }
+                }
+            }
+            .onAppear {
+                guard let selectedPreset = selectedPreset, let targetBottom = selectedPreset.targetBottom else { return }
+                let computedPercentage = state.computePercentage(target: targetBottom)
+                state.percentage = Double(truncating: computedPercentage as NSNumber)
+            }
+            .onDisappear {
+                if isEditSheetPresented == false {
+                    resetFields()
+                }
+            }
+        }
+
+        private func resetFields() {
+            state.newPresetName = ""
+            state.low = 0
+            state.duration = 0
+            state.percentage = 100 // Reset experimental slider if necessary
+        }
+
         private func presetView(for preset: TempTarget) -> some View {
             var low = preset.targetBottom
-            var high = preset.targetTop
             if state.units == .mmolL {
                 low = low?.asMmolL
-                high = high?.asMmolL
             }
+
             return HStack {
                 VStack {
                     HStack {
@@ -162,11 +267,13 @@ extension AddTempTarget {
                         Spacer()
                     }
                     HStack(spacing: 2) {
-                        Text(
-                            "\(formatter.string(from: (low ?? 0) as NSNumber)!) - \(formatter.string(from: (high ?? 0) as NSNumber)!)"
-                        )
-                        .foregroundColor(.secondary)
-                        .font(.caption)
+                        if let lowValue = low,
+                           let formattedLow = formatter.string(from: lowValue as NSNumber)
+                        {
+                            Text(formattedLow)
+                                .foregroundColor(.secondary)
+                                .font(.caption)
+                        }
 
                         Text(state.units.rawValue)
                             .foregroundColor(.secondary)
@@ -182,29 +289,12 @@ extension AddTempTarget {
                             .font(.caption)
 
                         Spacer()
-                    }.padding(.top, 2)
+                    }.padding(.bottom, 2)
                 }
                 .contentShape(Rectangle())
                 .onTapGesture {
                     state.enactPreset(id: preset.id)
                 }
-
-                Image(systemName: "xmark.circle").foregroundColor(.secondary)
-                    .contentShape(Rectangle())
-                    .padding(.vertical)
-                    .onTapGesture {
-                        removeAlert = Alert(
-                            title: Text("Are you sure?"),
-                            message: Text("Delete preset \"\(preset.displayName)\""),
-                            primaryButton: .destructive(Text("Delete"), action: { state.removePreset(id: preset.id) }),
-                            secondaryButton: .cancel()
-                        )
-                        isRemoveAlertPresented = true
-                    }
-                    .alert(isPresented: $isRemoveAlertPresented) {
-                        removeAlert!
-                    }
             }
-        }
-    }
+        } }
 }

+ 5 - 5
FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift

@@ -8,7 +8,7 @@ extension DataTable {
         @Injected() var carbsStorage: CarbsStorage!
         @Injected() var nightscoutManager: NightscoutManager!
         @Injected() var healthkitManager: HealthKitManager!
-        @Injected() var tidePoolManager: TidePoolManager!
+        @Injected() var tidepoolManager: TidepoolManager!
 
         func pumpHistory() -> [PumpHistoryEvent] {
             pumpHistoryStorage.recent()
@@ -33,10 +33,10 @@ extension DataTable {
         }
 
         func deleteCarbs(_ treatement: Treatment) {
-            // need to start with tidePool because Nightscout delete data
+            // need to start with tidepool because Nightscout delete data
             // probably to revise the logic
             // TODO:
-            tidePoolManager.deleteCarbs(
+            tidepoolManager.deleteCarbs(
                 at: treatement.date,
                 isFPU: treatement.isFPU,
                 fpuID: treatement.fpuID,
@@ -52,8 +52,8 @@ extension DataTable {
         }
 
         func deleteInsulin(_ treatement: Treatment) {
-            // delete tidePoolManager before NS - TODO
-            tidePoolManager.deleteInsulin(at: treatement.date)
+            // delete tidepoolManager before NS - TODO
+            tidepoolManager.deleteInsulin(at: treatement.date)
             nightscoutManager.deleteInsulin(at: treatement.date)
             if let id = treatement.idPumpEvent {
                 healthkitManager.deleteInsulin(syncID: id)

+ 20 - 0
FreeAPS/Sources/Modules/Home/HomeStateModel.swift

@@ -59,6 +59,7 @@ extension Home {
         @Published var displayYgridLines: Bool = false
         @Published var thresholdLines: Bool = false
         @Published var cgmAvailable: Bool = false
+        @Published var pumpStatusHighlightMessage: String? = nil
 
         let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
 
@@ -168,6 +169,7 @@ extension Home {
                     } else {
                         self.setupBattery()
                         self.setupReservoir()
+                        self.displaypumpStatusHighlightMessage()
                     }
                 }
                 .store(in: &lifetime)
@@ -348,6 +350,21 @@ extension Home {
             }
         }
 
+        /// Display the eventual status message provided by the manager of the pump
+        /// Only display if state is warning or critical message else return nil
+        private func displaypumpStatusHighlightMessage() {
+            DispatchQueue.main.async { [weak self] in
+                guard let self = self else { return }
+                if let statusHL = self.provider.deviceManager.pumpManager?.pumpStatusHighlight,
+                   statusHL.state == .warning || statusHL.state == .critical
+                {
+                    pumpStatusHighlightMessage = (statusHL.state == .warning ? "⚠️\n" : "‼️\n") + statusHL.localizedMessage
+                } else {
+                    pumpStatusHighlightMessage = nil
+                }
+            }
+        }
+
         private func setupCurrentTempTarget() {
             tempTarget = provider.tempTarget()
         }
@@ -412,6 +429,7 @@ extension Home.StateModel:
         setupBasals()
         setupBoluses()
         setupSuspensions()
+        displaypumpStatusHighlightMessage()
     }
 
     func pumpSettingsDidChange(_: PumpSettings) {
@@ -437,10 +455,12 @@ extension Home.StateModel:
 
     func pumpBatteryDidChange(_: Battery) {
         setupBattery()
+        displaypumpStatusHighlightMessage()
     }
 
     func pumpReservoirDidChange(_: Decimal) {
         setupReservoir()
+        displaypumpStatusHighlightMessage()
     }
 }
 

+ 103 - 50
FreeAPS/Sources/Modules/Home/View/Header/PumpView.swift

@@ -6,6 +6,7 @@ struct PumpView: View {
     @Binding var name: String
     @Binding var expiresAtDate: Date?
     @Binding var timerDate: Date
+    @Binding var pumpStatusHighlightMessage: String?
 
     private var reservoirFormatter: NumberFormatter {
         let formatter = NumberFormatter()
@@ -21,59 +22,67 @@ struct PumpView: View {
     }
 
     var body: some View {
-        VStack(alignment: .leading, spacing: 12) {
-            if reservoir == nil && battery == nil {
-                VStack(alignment: .center, spacing: 12) {
-                    HStack { // no cgm defined so display a generic CGM
-                        Image(systemName: "keyboard.onehanded.left").font(.body).imageScale(.large)
-                    }
+        if let pumpStatusHighlightMessage = pumpStatusHighlightMessage { // display message instead pump info
+            VStack(alignment: .center) {
+                Text(pumpStatusHighlightMessage).font(.footnote).fontWeight(.bold)
+                    .multilineTextAlignment(.center).frame(maxWidth: /*@START_MENU_TOKEN@*/ .infinity/*@END_MENU_TOKEN@*/)
+            }.frame(width: 100)
+        } else {
+            VStack(alignment: .leading, spacing: 12) {
+                if reservoir == nil && battery == nil {
+                    VStack(alignment: .center, spacing: 12) {
+                        HStack { // no cgm defined so display a generic CGM
+                            Image(systemName: "keyboard.onehanded.left").font(.body).imageScale(.large)
+                        }
+                        HStack {
+                            Text("Add pump").font(.caption).bold()
+                        }
+                    }.frame(alignment: .top)
+                }
+
+                if let reservoir = reservoir {
                     HStack {
-                        Text("Add pump").font(.caption).bold()
-                    }
-                }.frame(alignment: .top)
-            }
-
-            if let reservoir = reservoir {
-                HStack {
-                    Image(systemName: "drop.fill")
-                        .resizable()
-                        .aspectRatio(contentMode: .fit)
-                        .frame(maxHeight: 10)
-                        .foregroundColor(reservoirColor)
-                    if reservoir == 0xDEAD_BEEF {
-                        Text("50+ " + NSLocalizedString("U", comment: "Insulin unit")).font(.footnote)
+                        Image(systemName: "drop.fill")
+                            .resizable()
+                            .aspectRatio(contentMode: .fit)
+                            .frame(maxHeight: 10)
+                            .foregroundColor(reservoirColor)
+                        if reservoir == 0xDEAD_BEEF {
+                            Text("50+ " + NSLocalizedString("U", comment: "Insulin unit")).font(.footnote)
+                                .fontWeight(.bold)
+                        } else {
+                            Text(
+                                reservoirFormatter
+                                    .string(from: reservoir as NSNumber)! +
+                                    NSLocalizedString(" U", comment: "Insulin unit")
+                            )
+                            .font(.footnote).fontWeight(.bold)
+                        }
+                    }.frame(alignment: .top)
+                }
+                if let battery = battery, battery.display ?? false, expiresAtDate == nil {
+                    HStack {
+                        Image(systemName: "battery.100")
+                            .resizable()
+                            .aspectRatio(contentMode: .fit)
+                            .frame(maxHeight: 10)
+                            .foregroundColor(batteryColor)
+                        Text("\(Int(battery.percent ?? 100)) %").font(.footnote)
                             .fontWeight(.bold)
-                    } else {
-                        Text(
-                            reservoirFormatter
-                                .string(from: reservoir as NSNumber)! + NSLocalizedString(" U", comment: "Insulin unit")
-                        )
-                        .font(.footnote).fontWeight(.bold)
-                    }
-                }.frame(alignment: .top)
-            }
-            if let battery = battery, battery.display ?? false, expiresAtDate == nil {
-                HStack {
-                    Image(systemName: "battery.100")
-                        .resizable()
-                        .aspectRatio(contentMode: .fit)
-                        .frame(maxHeight: 10)
-                        .foregroundColor(batteryColor)
-                    Text("\(Int(battery.percent ?? 100)) %").font(.footnote)
-                        .fontWeight(.bold)
-                }.frame(alignment: .bottom)
-            }
+                    }.frame(alignment: .bottom)
+                }
 
-            if let date = expiresAtDate {
-                HStack {
-                    Image(systemName: "stopwatch.fill")
-                        .resizable()
-                        .aspectRatio(contentMode: .fit)
-                        .frame(maxHeight: 10)
-                        .foregroundColor(timerColor)
-                    Text(remainingTimeString(time: date.timeIntervalSince(timerDate))).font(.footnote)
-                        .fontWeight(.bold)
-                }.frame(alignment: .bottom)
+                if let date = expiresAtDate {
+                    HStack {
+                        Image(systemName: "stopwatch.fill")
+                            .resizable()
+                            .aspectRatio(contentMode: .fit)
+                            .frame(maxHeight: 10)
+                            .foregroundColor(timerColor)
+                        Text(remainingTimeString(time: date.timeIntervalSince(timerDate))).font(.footnote)
+                            .fontWeight(.bold)
+                    }.frame(alignment: .bottom)
+                }
             }
         }
     }
@@ -149,3 +158,47 @@ struct PumpView: View {
         }
     }
 }
+
+#Preview("message") {
+    PumpView(
+        reservoir: .constant(Decimal(10.0)),
+        battery: .constant(nil),
+        name: .constant("Pump test"),
+        expiresAtDate: .constant(Date().addingTimeInterval(24.hours)),
+        timerDate: .constant(Date()),
+        pumpStatusHighlightMessage: .constant("⚠️\n Insulin suspended")
+    )
+}
+
+#Preview("pump reservoir") {
+    PumpView(
+        reservoir: .constant(Decimal(40.0)),
+        battery: .constant(Battery(percent: 50, voltage: 2.0, string: BatteryState.normal, display: true)),
+        name: .constant("Pump test"),
+        expiresAtDate: .constant(nil),
+        timerDate: .constant(Date().addingTimeInterval(-24.hours)),
+        pumpStatusHighlightMessage: .constant(nil)
+    )
+}
+
+#Preview("pump expiration") {
+    PumpView(
+        reservoir: .constant(Decimal(10.0)),
+        battery: .constant(Battery(percent: 50, voltage: 2.0, string: BatteryState.normal, display: false)),
+        name: .constant("Pump test"),
+        expiresAtDate: .constant(Date().addingTimeInterval(2.hours)),
+        timerDate: .constant(Date().addingTimeInterval(2.hours)),
+        pumpStatusHighlightMessage: .constant(nil)
+    )
+}
+
+#Preview("no pump") {
+    PumpView(
+        reservoir: .constant(nil),
+        battery: .constant(nil),
+        name: .constant(""),
+        expiresAtDate: .constant(nil),
+        timerDate: .constant(Date()),
+        pumpStatusHighlightMessage: .constant(nil)
+    )
+}

+ 2 - 1
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -146,7 +146,8 @@ extension Home {
                 battery: $state.battery,
                 name: $state.pumpName,
                 expiresAtDate: $state.pumpExpiresAtDate,
-                timerDate: $state.timerDate
+                timerDate: $state.timerDate,
+                pumpStatusHighlightMessage: $state.pumpStatusHighlightMessage
             )
             .onTapGesture {
                 state.setupPump = true

+ 5 - 1
FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift

@@ -22,6 +22,7 @@ extension NightscoutConfig {
         @Published var connecting = false
         @Published var backfilling = false
         @Published var isUploadEnabled = false // Allow uploads
+        @Published var isDownloadEnabled = false // Allow downloads
         @Published var uploadGlucose = true // Upload Glucose
         @Published var changeUploadGlucose = true // if plugin, need to be change in CGM configuration
         @Published var useLocalSource = false
@@ -43,6 +44,7 @@ extension NightscoutConfig {
 
             subscribeSetting(\.allowAnnouncements, on: $allowAnnouncements) { allowAnnouncements = $0 }
             subscribeSetting(\.isUploadEnabled, on: $isUploadEnabled) { isUploadEnabled = $0 }
+            subscribeSetting(\.isDownloadEnabled, on: $isDownloadEnabled) { isDownloadEnabled = $0 }
             subscribeSetting(\.useLocalGlucoseSource, on: $useLocalSource) { useLocalSource = $0 }
             subscribeSetting(\.localGlucosePort, on: $localPort.map(Int.init)) { localPort = Decimal($0) }
             subscribeSetting(\.uploadGlucose, on: $uploadGlucose, initial: { uploadGlucose = $0 })
@@ -131,7 +133,9 @@ extension NightscoutConfig {
                 {
                     do {
                         let fetchedProfileStore = try jsonDecoder.decode([FetchedNightscoutProfileStore].self, from: data)
-                        guard let fetchedProfile: ScheduledNightscoutProfile = fetchedProfileStore.first?.store["default"]
+                        let loop = fetchedProfileStore.first?.enteredBy.contains("Loop")
+                        guard let fetchedProfile: FetchedNightscoutProfile = fetchedProfileStore.first?
+                            .store[loop! ? "Default" : "default"]
                         else {
                             error = "\nCan't find the default Nightscout Profile."
                             group.leave()

+ 42 - 90
FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutConfigRootView.swift

@@ -7,72 +7,36 @@ extension NightscoutConfig {
         let resolver: Resolver
         let displayClose: Bool
         @StateObject var state = StateModel()
-        @State var importAlert: Alert?
-        @State var isImportAlertPresented = false
-        @State var importedHasRun = false
+        @State private var importAlert: Alert?
+        @State private var isImportAlertPresented = false
+        @State private var importedHasRun = false
 
         @FetchRequest(
             entity: ImportError.entity(),
-            sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)], predicate: NSPredicate(
-                format: "date > %@", Date().addingTimeInterval(-1.minutes.timeInterval) as NSDate
-            )
+            sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)],
+            predicate: NSPredicate(format: "date > %@", Date().addingTimeInterval(-1.minutes.timeInterval) as NSDate)
         ) var fetchedErrors: FetchedResults<ImportError>
 
-        private var portFormater: NumberFormatter {
-            let formatter = NumberFormatter()
-            formatter.allowsFloats = false
-            return formatter
-        }
-
         var body: some View {
             Form {
-                Section {
-                    TextField("URL", text: $state.url)
-                        .disableAutocorrection(true)
-                        .textContentType(.URL)
-                        .autocapitalization(.none)
-                        .keyboardType(.URL)
-                    SecureField("API secret", text: $state.secret)
-                        .disableAutocorrection(true)
-                        .autocapitalization(.none)
-                        .textContentType(.password)
-                        .keyboardType(.asciiCapable)
-                    if !state.message.isEmpty {
-                        Text(state.message)
-                    }
-                    if state.connecting {
-                        HStack {
-                            Text("Connecting...")
-                            Spacer()
-                            ProgressView()
-                        }
-                    }
-                }
-
-                Section {
-                    Button("Connect") { state.connect() }
-                        .disabled(state.url.isEmpty || state.connecting)
-                    Button("Delete") { state.delete() }.foregroundColor(.red).disabled(state.connecting)
-                }
+                NavigationLink("Connect", destination: NightscoutConnectView(state: state))
+                NavigationLink("Upload", destination: NightscoutUploadView(state: state))
+                NavigationLink("Fetch and Remote Control", destination: NightscoutFetchView(state: state))
 
-                Section {
-                    Button("Open Nighstcout") {
-                        UIApplication.shared.open(URL(string: state.url)!, options: [:], completionHandler: nil)
-                    }
-                    .disabled(state.url.isEmpty || state.connecting)
-                }
-
-                Section {
-                    Toggle("Upload", isOn: $state.isUploadEnabled)
-                    if state.isUploadEnabled {
-                        Toggle("Glucose", isOn: $state.uploadGlucose).disabled(!state.changeUploadGlucose)
+                Section(
+                    header: Text("Import Settings from Nightscout"),
+                    footer: VStack(alignment: .leading, spacing: 2) {
+                        Text(
+                            "Importing settings from Nightscout will overwrite these settings in Trio Settings -> Configuration:"
+                        )
+                        Text(" • ") + Text("DIA (Pump settings)")
+                        Text(" • ") + Text("Basal Profile")
+                        Text(" • ") + Text("Insulin Sensitivities")
+                        Text(" • ") + Text("Carb Ratios")
+                        Text(" • ") + Text("Target Glucose")
                     }
-                } header: {
-                    Text("Allow Uploads")
-                }
-
-                Section {
-                    Button("Import settings from Nightscout") {
+                ) {
+                    Button("Import settings") {
                         importAlert = Alert(
                             title: Text("Import settings?"),
                             message: Text(
@@ -94,50 +58,38 @@ extension NightscoutConfig {
                         )
                         isImportAlertPresented.toggle()
                     }.disabled(state.url.isEmpty || state.connecting)
-
-                } header: { Text("Import from Nightscout") }
-
-                    .alert(isPresented: $importedHasRun) {
-                        Alert(
-                            title: Text((fetchedErrors.first?.error ?? "").count < 4 ? "Settings imported" : "Import Error"),
-                            message: Text(
-                                (fetchedErrors.first?.error ?? "").count < 4 ?
-                                    NSLocalizedString(
-                                        "\nNow please verify all of your new settings thoroughly:\n\n* Basal Settings\n * Carb Ratios\n * Glucose Targets\n * Insulin Sensitivities\n * DIA\n\n in Trio Settings > Configuration.\n\nBad or invalid profile settings could have disatrous effects.",
-                                        comment: "Imported Profiles Alert"
-                                    ) :
-                                    NSLocalizedString(fetchedErrors.first?.error ?? "", comment: "Import Error")
-                            ),
-                            primaryButton: .destructive(
-                                Text("OK")
-                            ),
-                            secondaryButton: .cancel()
-                        )
-                    }
-
-                Section {
-                    Toggle("Use local glucose server", isOn: $state.useLocalSource)
-                    HStack {
-                        Text("Port")
-                        DecimalTextField("", value: $state.localPort, formatter: portFormater)
-                    }
-                } header: { Text("Local glucose source") }
+                        .alert(isPresented: $importedHasRun) {
+                            Alert(
+                                title: Text((fetchedErrors.first?.error ?? "").count < 4 ? "Settings imported" : "Import Error"),
+                                message: Text(
+                                    (fetchedErrors.first?.error ?? "").count < 4 ?
+                                        NSLocalizedString(
+                                            "\nNow please verify all of your new settings thoroughly: \n\n • DIA (Pump settings)\n • Basal Profile\n • Insulin Sensitivities\n • Carb Ratios\n • Target Glucose\n\n in Trio Settings -> Configuration.\n\nBad or invalid profile settings could have disastrous effects.",
+                                            comment: "Imported Profiles Alert"
+                                        ) :
+                                        NSLocalizedString(fetchedErrors.first?.error ?? "", comment: "Import Error")
+                                ),
+                                primaryButton: .destructive(
+                                    Text("OK")
+                                ),
+                                secondaryButton: .cancel()
+                            )
+                        }
+                }
                 Section {
                     Button("Backfill glucose") { state.backfillGlucose() }
                         .disabled(state.url.isEmpty || state.connecting || state.backfilling)
+                } header: { Text("Backfill glucose from Nightscout")
                 }
-
-                Section {
-                    Toggle("Remote control", isOn: $state.allowAnnouncements)
-                } header: { Text("Allow Remote control of Trio") }
             }
-            .onAppear(perform: configureView)
             .navigationBarTitle("Nightscout Config")
             .navigationBarTitleDisplayMode(.automatic)
             .navigationBarItems(leading: displayClose ? Button("Close", action: state.hideModal) : nil)
             .alert(isPresented: $isImportAlertPresented) {
                 importAlert!
             }
+
+            .onAppear(perform: configureView)
         }
     }
 }

+ 58 - 0
FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutConnectView.swift

@@ -0,0 +1,58 @@
+import SwiftUI
+
+struct NightscoutConnectView: View {
+    @ObservedObject var state: NightscoutConfig.StateModel
+    @State private var portFormater: NumberFormatter
+
+    init(state: NightscoutConfig.StateModel) {
+        self.state = state
+        portFormater = NumberFormatter()
+        portFormater.allowsFloats = false
+    }
+
+    var body: some View {
+        Form {
+            Section {
+                TextField("URL", text: $state.url)
+                    .disableAutocorrection(true)
+                    .textContentType(.URL)
+                    .autocapitalization(.none)
+                    .keyboardType(.URL)
+                SecureField("API secret", text: $state.secret)
+                    .disableAutocorrection(true)
+                    .autocapitalization(.none)
+                    .textContentType(.password)
+                    .keyboardType(.asciiCapable)
+                if !state.message.isEmpty {
+                    Text(state.message)
+                }
+                if state.connecting {
+                    HStack {
+                        Text("Connecting...")
+                        Spacer()
+                        ProgressView()
+                    }
+                }
+            }
+            Section {
+                Button("Connect to Nightscout") { state.connect() }
+                    .disabled(state.url.isEmpty || state.connecting)
+                Button("Delete") { state.delete() }.foregroundColor(.red).disabled(state.connecting)
+            }
+            Section {
+                Button("Open Nightscout") {
+                    UIApplication.shared.open(URL(string: state.url)!, options: [:], completionHandler: nil)
+                }
+                .disabled(state.url.isEmpty || state.connecting)
+            }
+            Section {
+                Toggle("Use local glucose server", isOn: $state.useLocalSource)
+                HStack {
+                    Text("Port")
+                    DecimalTextField("", value: $state.localPort, formatter: portFormater)
+                }
+            } header: { Text("Local glucose source") }
+        }
+        .navigationTitle("Connect")
+    }
+}

+ 41 - 0
FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutFetchView.swift

@@ -0,0 +1,41 @@
+
+import SwiftUI
+
+struct NightscoutFetchView: View {
+    @ObservedObject var state: NightscoutConfig.StateModel
+
+    var body: some View {
+        Form {
+            Section {
+                Toggle("Fetch Treatments", isOn: $state.isDownloadEnabled)
+                    .onChange(of: state.isDownloadEnabled) { newValue in
+                        if !newValue {
+                            state.allowAnnouncements = false
+                        }
+                    }
+            } header: {
+                Text("Allow Fetching from Nightscout")
+            } footer: {
+                Text(
+                    "The Fetch Treatments toggle enables fetching of carbs and temp targets entered in Careportal or by another uploading device than Trio."
+                )
+            }
+            Section(
+                header: Text("Allow Remote control of Trio"),
+                footer: VStack(alignment: .leading, spacing: 2) {
+                    Text("Fetch Treatments needs to be allowed to be able to toggle on Remote Control.")
+                    Text("\nWhen enabled you allow these remote functions through announcements from Nightscout:")
+                    Text(" • ") + Text("Suspend/Resume Pump")
+                    Text(" • ") + Text("Opening/Closing Loop")
+                    Text(" • ") + Text("Set Temp Basal")
+                    Text(" • ") + Text("Enact Bolus")
+                }
+            )
+                {
+                    Toggle("Remote Control", isOn: $state.allowAnnouncements)
+                        .disabled(!state.isDownloadEnabled)
+                }
+        }
+        .navigationTitle("Fetch and Remote")
+    }
+}

+ 26 - 0
FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutUploadView.swift

@@ -0,0 +1,26 @@
+
+import SwiftUI
+
+struct NightscoutUploadView: View {
+    @ObservedObject var state: NightscoutConfig.StateModel
+
+    var body: some View {
+        Form {
+            Section(
+                header: Text("Allow Uploading to Nightscout"),
+                footer: VStack(alignment: .leading, spacing: 2) {
+                    Text(
+                        "The Upload Treatments toggle enables uploading of carbs, temp targets, device status, preferences and settings."
+                    )
+                    Text("\nThe Upload Glucose toggle enables uploading of CGM readings.")
+                }
+            )
+                {
+                    Toggle("Upload Treatments and Settings", isOn: $state.isUploadEnabled)
+
+                    Toggle("Upload Glucose", isOn: $state.uploadGlucose).disabled(!state.changeUploadGlucose)
+                }
+        }
+        .navigationTitle("Upload")
+    }
+}

+ 116 - 1
FreeAPS/Sources/Modules/OverrideProfilesConfig/OverrideProfilesStateModel.swift

@@ -29,6 +29,24 @@ extension OverrideProfilesConfig {
 
         var units: GlucoseUnits = .mmolL
 
+        private var formatter: NumberFormatter {
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            formatter.maximumFractionDigits = 0
+            return formatter
+        }
+
+        private var glucoseFormatter: NumberFormatter {
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            formatter.maximumFractionDigits = 0
+            if units == .mmolL {
+                formatter.maximumFractionDigits = 1
+            }
+            formatter.roundingMode = .halfUp
+            return formatter
+        }
+
         override func subscribe() {
             units = settingsManager.settings.units
             defaultSmbMinutes = settingsManager.preferences.maxSMBBasalMinutes
@@ -38,6 +56,59 @@ extension OverrideProfilesConfig {
 
         let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
 
+        struct ProfileViewData {
+            let target: Decimal
+            let duration: Decimal
+            let name: String
+            let percent: Double
+            let perpetual: Bool
+            let durationString: String
+            let scheduledSMBString: String
+            let smbString: String
+            let targetString: String
+            let maxMinutesSMB: Decimal
+            let maxMinutesUAM: Decimal
+            let isfString: String
+            let crString: String
+            let isfAndCRString: String
+        }
+
+        func profileViewData(for preset: OverridePresets) -> ProfileViewData {
+            let target = units == .mmolL ? (((preset.target ?? 0) as NSDecimalNumber) as Decimal)
+                .asMmolL : (preset.target ?? 0) as Decimal
+            let duration = (preset.duration ?? 0) as Decimal
+            let name = ((preset.name ?? "") == "") || (preset.name?.isEmpty ?? true) ? "" : preset.name!
+            let percent = preset.percentage / 100
+            let perpetual = preset.indefinite
+            let durationString = perpetual ? "" : "\(formatter.string(from: duration as NSNumber)!)"
+            let scheduledSMBString = (preset.smbIsOff && preset.smbIsScheduledOff) ? "Scheduled SMBs" : ""
+            let smbString = (preset.smbIsOff && scheduledSMBString == "") ? "SMBs are off" : ""
+            let targetString = target != 0 ? "\(glucoseFormatter.string(from: target as NSNumber)!)" : ""
+            let maxMinutesSMB = (preset.smbMinutes as Decimal?) != nil ? (preset.smbMinutes ?? 0) as Decimal : 0
+            let maxMinutesUAM = (preset.uamMinutes as Decimal?) != nil ? (preset.uamMinutes ?? 0) as Decimal : 0
+            let isfString = preset.isf ? "ISF" : ""
+            let crString = preset.cr ? "CR" : ""
+            let dash = crString != "" ? "/" : ""
+            let isfAndCRString = isfString + dash + crString
+
+            return ProfileViewData(
+                target: target,
+                duration: duration,
+                name: name,
+                percent: percent,
+                perpetual: perpetual,
+                durationString: durationString,
+                scheduledSMBString: scheduledSMBString,
+                smbString: smbString,
+                targetString: targetString,
+                maxMinutesSMB: maxMinutesSMB,
+                maxMinutesUAM: maxMinutesUAM,
+                isfString: isfString,
+                crString: crString,
+                isfAndCRString: isfAndCRString
+            )
+        }
+
         func saveSettings() {
             coredataContext.perform { [self] in
                 let saveOverride = Override(context: self.coredataContext)
@@ -87,6 +158,7 @@ extension OverrideProfilesConfig {
                 saveOverride.percentage = self.percentage
                 saveOverride.smbIsOff = self.smbIsOff
                 saveOverride.name = self.profileName
+                self.profileName = ""
                 id = UUID().uuidString
                 self.isPreset.toggle()
                 saveOverride.id = id
@@ -166,7 +238,6 @@ extension OverrideProfilesConfig {
                 let requestEnabled = Override.fetchRequest() as NSFetchRequest<Override>
                 let sortIsEnabled = NSSortDescriptor(key: "date", ascending: false)
                 requestEnabled.sortDescriptors = [sortIsEnabled]
-                // requestEnabled.fetchLimit = 1
                 try? overrideArray = coredataContext.fetch(requestEnabled)
                 isEnabled = overrideArray.first?.enabled ?? false
                 percentage = overrideArray.first?.percentage ?? 100
@@ -229,6 +300,29 @@ extension OverrideProfilesConfig {
             }
         }
 
+        func populateSettings(from preset: OverridePresets) {
+            profileName = preset.name ?? ""
+            percentage = preset.percentage
+            duration = (preset.duration ?? 0) as Decimal
+            _indefinite = preset.indefinite
+            override_target = preset.target != nil
+            if let targetValue = preset.target as NSDecimalNumber? {
+                target = units == .mmolL ? (targetValue as Decimal).asMmolL : targetValue as Decimal
+            } else {
+                target = 0
+            }
+            advancedSettings = preset.advancedSettings
+            smbIsOff = preset.smbIsOff
+            smbIsScheduledOff = preset.smbIsScheduledOff
+            isf = preset.isf
+            cr = preset.cr
+            smbMinutes = (preset.smbMinutes ?? 0) as Decimal
+            uamMinutes = (preset.uamMinutes ?? 0) as Decimal
+            isfAndCr = preset.isfAndCr
+            start = (preset.start ?? 0) as Decimal
+            end = (preset.end ?? 0) as Decimal
+        }
+
         func cancelProfile() {
             _indefinite = true
             isEnabled = false
@@ -247,5 +341,26 @@ extension OverrideProfilesConfig {
             smbMinutes = defaultSmbMinutes
             uamMinutes = defaultUamMinutes
         }
+
+        func updatePreset(_ preset: OverridePresets) {
+            let context = CoreDataStack.shared.persistentContainer.viewContext
+            context.performAndWait {
+                preset.name = profileName
+                preset.percentage = percentage
+                preset.duration = NSDecimalNumber(decimal: duration)
+                let targetValue = override_target ? (units == .mmolL ? target.asMgdL : target) : nil
+                preset.target = targetValue != nil ? NSDecimalNumber(decimal: targetValue!) : nil
+                preset.indefinite = _indefinite
+                preset.advancedSettings = advancedSettings
+                preset.smbIsOff = smbIsOff
+                preset.smbIsScheduledOff = smbIsScheduledOff
+                preset.isf = isf
+                preset.cr = cr
+                preset.smbMinutes = NSDecimalNumber(decimal: smbMinutes)
+                preset.uamMinutes = NSDecimalNumber(decimal: uamMinutes)
+                preset.isfAndCr = isfAndCr
+                try? context.save()
+            }
+        }
     }
 }

+ 271 - 128
FreeAPS/Sources/Modules/OverrideProfilesConfig/View/OverrideProfilesRootView.swift

@@ -1,4 +1,5 @@
 import CoreData
+import Foundation
 import SwiftUI
 import Swinject
 
@@ -10,8 +11,14 @@ extension OverrideProfilesConfig {
         @State private var isEditing = false
         @State private var showAlert = false
         @State private var showingDetail = false
+        @State private var selectedPreset: OverridePresets?
+        @State private var isEditSheetPresented: Bool = false
         @State private var alertSring = ""
         @State var isSheetPresented: Bool = false
+        @State private var originalPreset: OverridePresets?
+        @State private var showDeleteAlert = false
+        @State private var indexToDelete: Int?
+        @State private var profileNameToDelete: String = ""
 
         @Environment(\.dismiss) var dismiss
         @Environment(\.managedObjectContext) var moc
@@ -22,6 +29,7 @@ extension OverrideProfilesConfig {
                 format: "name != %@", "" as String
             )
         ) var fetchedProfiles: FetchedResults<OverridePresets>
+        var units: GlucoseUnits = .mmolL
 
         private var formatter: NumberFormatter {
             let formatter = NumberFormatter()
@@ -43,144 +51,246 @@ extension OverrideProfilesConfig {
 
         var presetPopover: some View {
             Form {
-                Section {
-                    TextField("Name Of Profile", text: $state.profileName)
-                } header: { Text("Enter Name of Profile") }
-
+                nameSection(header: "Enter a name")
+                settingsSection(header: "Settings to save")
                 Section {
                     Button("Save") {
                         state.savePreset()
                         isSheetPresented = false
                     }
-                    .disabled(state.profileName.isEmpty || fetchedProfiles.filter({ $0.name == state.profileName }).isNotEmpty)
+                    .disabled(
+                        state.profileName.isEmpty || fetchedProfiles
+                            .contains(where: { $0.name == state.profileName })
+                    )
 
                     Button("Cancel") {
                         isSheetPresented = false
                     }
+                    .tint(.red)
                 }
             }
         }
 
-        var body: some View {
+        var editPresetPopover: some View {
             Form {
-                if state.presets.isNotEmpty {
-                    Section {
-                        ForEach(fetchedProfiles) { preset in
-                            profilesView(for: preset)
-                        }.onDelete(perform: removeProfile)
+                nameSection(header: "Change name?")
+                settingsConfig(header: "Change settings")
+                Section {
+                    Button("Save") {
+                        guard let selectedPreset = selectedPreset else { return }
+                        state.updatePreset(selectedPreset)
+                        isEditSheetPresented = false
+                    }
+                    .disabled(!hasChanges())
+
+                    Button("Cancel") {
+                        isEditSheetPresented = false
                     }
+                    .tint(.red)
                 }
-                Section {
-                    VStack {
-                        Slider(
-                            value: $state.percentage,
-                            in: 10 ... 200,
-                            step: 1,
-                            onEditingChanged: { editing in
-                                isEditing = editing
-                            }
-                        ).accentColor(state.percentage >= 130 ? .red : .blue)
-                        Text("\(state.percentage.formatted(.number)) %")
-                            .foregroundColor(
-                                state
-                                    .percentage >= 130 ? .red :
-                                    (isEditing ? .orange : .blue)
-                            )
-                            .font(.largeTitle)
-                        Spacer()
-                        Toggle(isOn: $state._indefinite) {
-                            Text("Enable indefinitely")
+            }
+            .onAppear {
+                if let preset = selectedPreset {
+                    originalPreset = preset
+                    state.populateSettings(from: preset)
+                }
+            }
+            .onDisappear {
+                state.savedSettings()
+            }
+        }
+
+        @ViewBuilder private func nameSection(header: String) -> some View {
+            Section {
+                TextField("Profile override name", text: $state.profileName)
+            } header: {
+                Text(header)
+            }
+        }
+
+        @ViewBuilder private func settingsConfig(header: String) -> some View {
+            Section {
+                VStack {
+                    Spacer()
+                    Text("\(state.percentage.formatted(.number)) %")
+                        .foregroundColor(
+                            state
+                                .percentage >= 130 ? .red :
+                                (isEditing ? .orange : .blue)
+                        )
+                        .font(.largeTitle)
+                    Slider(
+                        value: $state.percentage,
+                        in: 10 ... 200,
+                        step: 1,
+                        onEditingChanged: { editing in
+                            isEditing = editing
                         }
+                    ).accentColor(state.percentage >= 130 ? .red : .blue)
+                    Spacer()
+                    Toggle(isOn: $state._indefinite) {
+                        Text("Enable indefinitely")
                     }
-                    if !state._indefinite {
-                        HStack {
-                            Text("Duration")
-                            DecimalTextField("0", value: $state.duration, formatter: formatter, cleanInput: false)
-                            Text("minutes").foregroundColor(.secondary)
-                        }
+                }
+                if !state._indefinite {
+                    HStack {
+                        Text("Duration")
+                        DecimalTextField("0", value: $state.duration, formatter: formatter, cleanInput: false)
+                        Text("minutes").foregroundColor(.secondary)
                     }
+                }
 
+                HStack {
+                    Toggle(isOn: $state.override_target) {
+                        Text("Override Profile Target")
+                    }
+                }
+                if state.override_target {
                     HStack {
-                        Toggle(isOn: $state.override_target) {
-                            Text("Override Profile Target")
-                        }
+                        Text("Target Glucose")
+                        DecimalTextField("0", value: $state.target, formatter: glucoseFormatter, cleanInput: false)
+                        Text(state.units.rawValue).foregroundColor(.secondary)
                     }
-                    if state.override_target {
-                        HStack {
-                            Text("Target Glucose")
-                            DecimalTextField("0", value: $state.target, formatter: glucoseFormatter, cleanInput: false)
-                            Text(state.units.rawValue).foregroundColor(.secondary)
-                        }
+                }
+                HStack {
+                    Toggle(isOn: $state.advancedSettings) {
+                        Text("More options")
                     }
+                }
+                if state.advancedSettings {
                     HStack {
-                        Toggle(isOn: $state.advancedSettings) {
-                            Text("More options")
+                        Toggle(isOn: $state.smbIsOff) {
+                            Text("Always Disable SMBs")
                         }
                     }
-                    if state.advancedSettings {
-                        HStack {
-                            Toggle(isOn: $state.smbIsOff) {
-                                Text("Always Disable SMBs")
-                            }
-                        }
-                        if !state.smbIsOff {
-                            HStack {
-                                Toggle(isOn: $state.smbIsScheduledOff) {
-                                    Text("Schedule when SMBs are Off")
-                                }
-                            }
-                            if state.smbIsScheduledOff {
-                                HStack {
-                                    Text("First Hour SMBs are Off (24 hours)")
-                                    DecimalTextField("0", value: $state.start, formatter: formatter, cleanInput: false)
-                                    Text("hour").foregroundColor(.secondary)
-                                }
-                                HStack {
-                                    Text("First Hour SMBs are Resumed (24 hours)")
-                                    DecimalTextField("0", value: $state.end, formatter: formatter, cleanInput: false)
-                                    Text("hour").foregroundColor(.secondary)
-                                }
-                            }
-                        }
+                    if !state.smbIsOff {
                         HStack {
-                            Toggle(isOn: $state.isfAndCr) {
-                                Text("Change ISF and CR")
+                            Toggle(isOn: $state.smbIsScheduledOff) {
+                                Text("Schedule when SMBs are Off")
                             }
                         }
-                        if !state.isfAndCr {
+                        if state.smbIsScheduledOff {
                             HStack {
-                                Toggle(isOn: $state.isf) {
-                                    Text("Change ISF")
-                                }
+                                Text("First Hour SMBs are Off (24 hours)")
+                                DecimalTextField("0", value: $state.start, formatter: formatter, cleanInput: false)
+                                Text("hour").foregroundColor(.secondary)
                             }
                             HStack {
-                                Toggle(isOn: $state.cr) {
-                                    Text("Change CR")
-                                }
+                                Text("First Hour SMBs are Resumed (24 hours)")
+                                DecimalTextField("0", value: $state.end, formatter: formatter, cleanInput: false)
+                                Text("hour").foregroundColor(.secondary)
                             }
                         }
+                    }
+                    HStack {
+                        Toggle(isOn: $state.isfAndCr) {
+                            Text("Change ISF and CR")
+                        }
+                    }
+                    if !state.isfAndCr {
                         HStack {
-                            Text("SMB Minutes")
-                            DecimalTextField(
-                                "0",
-                                value: $state.smbMinutes,
-                                formatter: formatter,
-                                cleanInput: false
-                            )
-                            Text("minutes").foregroundColor(.secondary)
+                            Toggle(isOn: $state.isf) {
+                                Text("Change ISF")
+                            }
                         }
                         HStack {
-                            Text("UAM SMB Minutes")
-                            DecimalTextField(
-                                "0",
-                                value: $state.uamMinutes,
-                                formatter: formatter,
-                                cleanInput: false
-                            )
-                            Text("minutes").foregroundColor(.secondary)
+                            Toggle(isOn: $state.cr) {
+                                Text("Change CR")
+                            }
                         }
                     }
+                    HStack {
+                        Text("SMB Minutes")
+                        DecimalTextField(
+                            "0",
+                            value: $state.smbMinutes,
+                            formatter: formatter,
+                            cleanInput: false
+                        )
+                        Text("minutes").foregroundColor(.secondary)
+                    }
+                    HStack {
+                        Text("UAM SMB Minutes")
+                        DecimalTextField(
+                            "0",
+                            value: $state.uamMinutes,
+                            formatter: formatter,
+                            cleanInput: false
+                        )
+                        Text("minutes").foregroundColor(.secondary)
+                    }
+                }
+            } header: {
+                Text(header)
+            }
+        }
+
+        @ViewBuilder private func settingsSection(header: String) -> some View {
+            Section(header: Text(header)) {
+                let percentString = Text("Override: \(Int(state.percentage))%")
+                let targetString = state
+                    .target != 0 ? Text("Target: \(state.target.formatted()) \(state.units.rawValue)") : Text("")
+                let durationString = state
+                    ._indefinite ? Text("Duration: Indefinite") : Text("Duration: \(state.duration.formatted()) minutes")
+                let isfString = state.isf ? Text("Change ISF") : Text("")
+                let crString = state.cr ? Text("Change CR") : Text("")
+                let smbString = state.smbIsOff ? Text("Disable SMB") : Text("")
+                let scheduledSMBString = state.smbIsScheduledOff ? Text("SMB Schedule On") : Text("")
+                let maxMinutesSMBString = state
+                    .smbMinutes != 0 ? Text("\(state.smbMinutes.formatted()) SMB Basal minutes") : Text("")
+                let maxMinutesUAMString = state
+                    .uamMinutes != 0 ? Text("\(state.uamMinutes.formatted()) UAM Basal minutes") : Text("")
+
+                VStack(alignment: .leading, spacing: 2) {
+                    percentString
+                    if targetString != Text("") { targetString }
+                    if durationString != Text("") { durationString }
+                    if isfString != Text("") { isfString }
+                    if crString != Text("") { crString }
+                    if smbString != Text("") { smbString }
+                    if scheduledSMBString != Text("") { scheduledSMBString }
+                    if maxMinutesSMBString != Text("") { maxMinutesSMBString }
+                    if maxMinutesUAMString != Text("") { maxMinutesUAMString }
+                }
+                .foregroundColor(.secondary)
+                .font(.caption)
+            }
+        }
+
+        var body: some View {
+            Form {
+                if state.presets.isNotEmpty {
+                    Section {
+                        ForEach(fetchedProfiles.indices, id: \.self) { index in
+                            let preset = fetchedProfiles[index]
+                            profilesView(for: preset)
+                                .swipeActions {
+                                    Button(role: .none) {
+                                        indexToDelete = index
+                                        profileNameToDelete = preset.name ?? "this profile"
+                                        showDeleteAlert = true
+                                    } label: {
+                                        Label("Delete", systemImage: "trash")
+                                    }.tint(.red)
 
+                                    Button {
+                                        selectedPreset = preset
+                                        state.profileName = preset.name ?? ""
+                                        isEditSheetPresented = true
+                                    } label: {
+                                        Label("Edit", systemImage: "square.and.pencil")
+                                    }.tint(.blue)
+                                }
+                        }
+                    }
+                    header: { Text("Activate profile override") }
+                    footer: { VStack(alignment: .leading) {
+                        Text("Swipe left on a profile to edit or delete it.")
+                    }
+                    }
+                }
+                settingsConfig(header: "Insulin")
+                Section {
                     HStack {
                         Button("Start new Profile") {
                             showAlert.toggle()
@@ -244,6 +354,7 @@ extension OverrideProfilesConfig {
                             .tint(.orange)
                             .frame(maxWidth: .infinity, alignment: .trailing)
                             .buttonStyle(BorderlessButtonStyle())
+                            .font(.callout)
                             .controlSize(.mini)
                             .disabled(unChanged())
                     }
@@ -251,8 +362,6 @@ extension OverrideProfilesConfig {
                         presetPopover
                     }
                 }
-
-                header: { Text("Insulin") }
                 footer: {
                     Text(
                         "Your profile basal insulin will be adjusted with the override percentage and your profile ISF and CR will be inversly adjusted with the percentage."
@@ -273,46 +382,47 @@ extension OverrideProfilesConfig {
             .navigationBarTitle("Profiles")
             .navigationBarTitleDisplayMode(.automatic)
             .navigationBarItems(leading: Button("Close", action: state.hideModal))
+            .sheet(isPresented: $isEditSheetPresented) {
+                editPresetPopover
+                    .padding()
+            }
+            .alert(isPresented: $showDeleteAlert) {
+                Alert(
+                    title: Text("Delete profile override"),
+                    message: Text("Are you sure you want to delete\n\(profileNameToDelete)?"),
+                    primaryButton: .destructive(Text("Delete")) {
+                        if let index = indexToDelete {
+                            removeProfile(at: IndexSet(integer: index))
+                        }
+                    },
+                    secondaryButton: .cancel()
+                )
+            }
         }
 
         @ViewBuilder private func profilesView(for preset: OverridePresets) -> some View {
-            let target = state.units == .mmolL ? (((preset.target ?? 0) as NSDecimalNumber) as Decimal)
-                .asMmolL : (preset.target ?? 0) as Decimal
-            let duration = (preset.duration ?? 0) as Decimal
-            let name = ((preset.name ?? "") == "") || (preset.name?.isEmpty ?? true) ? "" : preset.name!
-            let percent = preset.percentage / 100
-            let perpetual = preset.indefinite
-            let durationString = perpetual ? "" : "\(formatter.string(from: duration as NSNumber)!)"
-            let scheduledSMBstring = (preset.smbIsOff && preset.smbIsScheduledOff) ? "Scheduled SMBs" : ""
-            let smbString = (preset.smbIsOff && scheduledSMBstring == "") ? "SMBs are off" : ""
-            let targetString = target != 0 ? "\(glucoseFormatter.string(from: target as NSNumber)!)" : ""
-            let maxMinutesSMB = (preset.smbMinutes as Decimal?) != nil ? (preset.smbMinutes ?? 0) as Decimal : 0
-            let maxMinutesUAM = (preset.uamMinutes as Decimal?) != nil ? (preset.uamMinutes ?? 0) as Decimal : 0
-            let isfString = preset.isf ? "ISF" : ""
-            let crString = preset.cr ? "CR" : ""
-            let dash = crString != "" ? "/" : ""
-            let isfAndCRstring = isfString + dash + crString
+            let data = state.profileViewData(for: preset)
 
-            if name != "" {
+            if data.name != "" {
                 HStack {
                     VStack {
                         HStack {
-                            Text(name)
+                            Text(data.name)
                             Spacer()
                         }
                         HStack(spacing: 5) {
-                            Text(percent.formatted(.percent.grouping(.never).rounded().precision(.fractionLength(0))))
-                            if targetString != "" {
-                                Text(targetString)
-                                Text(targetString != "" ? state.units.rawValue : "")
+                            Text(data.percent.formatted(.percent.grouping(.never).rounded().precision(.fractionLength(0))))
+                            if data.targetString != "" {
+                                Text(data.targetString)
+                                Text(data.targetString != "" ? state.units.rawValue : "")
                             }
-                            if durationString != "" { Text(durationString + (perpetual ? "" : "min")) }
-                            if smbString != "" { Text(smbString).foregroundColor(.secondary).font(.caption) }
-                            if scheduledSMBstring != "" { Text(scheduledSMBstring) }
+                            if data.durationString != "" { Text(data.durationString + (data.perpetual ? "" : "min")) }
+                            if data.smbString != "" { Text(data.smbString).foregroundColor(.secondary).font(.caption) }
+                            if data.scheduledSMBString != "" { Text(data.scheduledSMBString) }
                             if preset.advancedSettings {
-                                Text(maxMinutesSMB == 0 ? "" : maxMinutesSMB.formatted() + " SMB")
-                                Text(maxMinutesUAM == 0 ? "" : maxMinutesUAM.formatted() + " UAM")
-                                Text(isfAndCRstring)
+                                Text(data.maxMinutesSMB == 0 ? "" : data.maxMinutesSMB.formatted() + " SMB")
+                                Text(data.maxMinutesUAM == 0 ? "" : data.maxMinutesUAM.formatted() + " UAM")
+                                Text(data.isfAndCRString)
                             }
                             Spacer()
                         }
@@ -339,6 +449,39 @@ extension OverrideProfilesConfig {
             return defaultProfile || noDurationSpecified || targetZeroWithOverride || allSettingsDefault
         }
 
+        private func hasChanges() -> Bool {
+            guard let originalPreset = originalPreset else { return false }
+
+            let targetInStateUnits: Decimal
+            let targetInPresetUnits: Decimal
+
+            if state.units == .mmolL {
+                targetInStateUnits = state.target
+                targetInPresetUnits = (originalPreset.target as NSDecimalNumber?)?.decimalValue.asMmolL ?? 0
+            } else {
+                targetInStateUnits = state.target
+                targetInPresetUnits = (originalPreset.target as NSDecimalNumber?)?.decimalValue ?? 0
+            }
+
+            let hasChanges = state.profileName != originalPreset.name ||
+                state.percentage != originalPreset.percentage ||
+                state.duration != (originalPreset.duration ?? 0) as Decimal ||
+                state._indefinite != originalPreset.indefinite ||
+                state.override_target != (originalPreset.target != nil) ||
+                (state.override_target && targetInStateUnits != targetInPresetUnits) ||
+                state.smbIsOff != originalPreset.smbIsOff ||
+                state.smbIsScheduledOff != originalPreset.smbIsScheduledOff ||
+                state.isf != originalPreset.isf ||
+                state.cr != originalPreset.cr ||
+                state.smbMinutes != (originalPreset.smbMinutes ?? 0) as Decimal ||
+                state.uamMinutes != (originalPreset.uamMinutes ?? 0) as Decimal ||
+                state.isfAndCr != originalPreset.isfAndCr ||
+                state.start != (originalPreset.start ?? 0) as Decimal ||
+                state.end != (originalPreset.end ?? 0) as Decimal
+
+            return hasChanges
+        }
+
         private func removeProfile(at offsets: IndexSet) {
             for index in offsets {
                 let language = fetchedProfiles[index]

+ 0 - 2
FreeAPS/Sources/Modules/PreferencesEditor/PreferencesEditorStateModel.swift

@@ -4,14 +4,12 @@ import SwiftUI
 extension PreferencesEditor {
     final class StateModel: BaseStateModel<Provider>, PreferencesSettable { private(set) var preferences = Preferences()
         @Published var unitsIndex = 1
-        @Published var allowAnnouncements = false
         @Published var insulinReqPercentage: Decimal = 70
         @Published var skipBolusScreenAfterCarbs = false
         @Published var sections: [FieldSection] = []
 
         override func subscribe() {
             preferences = provider.preferences
-            subscribeSetting(\.allowAnnouncements, on: $allowAnnouncements) { allowAnnouncements = $0 }
             subscribeSetting(\.insulinReqPercentage, on: $insulinReqPercentage) { insulinReqPercentage = $0 }
             subscribeSetting(\.skipBolusScreenAfterCarbs, on: $skipBolusScreenAfterCarbs) { skipBolusScreenAfterCarbs = $0 }
 

+ 0 - 3
FreeAPS/Sources/Modules/PreferencesEditor/View/PreferencesEditorRootView.swift

@@ -27,9 +27,6 @@ extension PreferencesEditor {
                         Text("mg/dL").tag(0)
                         Text("mmol/L").tag(1)
                     }
-
-                    Toggle("Remote control", isOn: $state.allowAnnouncements)
-
                     HStack {
                         Text("Recommended Bolus Percentage")
                         DecimalTextField("", value: $state.insulinReqPercentage, formatter: formatter)

+ 1 - 1
FreeAPS/Sources/Modules/Settings/SettingsProvider.swift

@@ -1,5 +1,5 @@
 extension Settings {
     final class Provider: BaseProvider, SettingsProvider {
-        @Injected() var tidePoolManager: TidePoolManager!
+        @Injected() var tidepoolManager: TidepoolManager!
     }
 }

+ 4 - 4
FreeAPS/Sources/Modules/Settings/SettingsStateModel.swift

@@ -14,7 +14,7 @@ extension Settings {
         @Published var debugOptions = false
         @Published var animatedBackground = false
         @Published var serviceUIType: ServiceUI.Type?
-        @Published var setupTidePool = false
+        @Published var setupTidepool = false
 
         private(set) var buildNumber = ""
         private(set) var versionNumber = ""
@@ -75,7 +75,7 @@ extension Settings.StateModel: SettingsObserver {
 extension Settings.StateModel: ServiceOnboardingDelegate {
     func serviceOnboarding(didCreateService service: Service) {
         debug(.nightscout, "Service with identifier \(service.pluginIdentifier) created")
-        provider.tidePoolManager.addTidePoolService(service: service)
+        provider.tidepoolManager.addTidepoolService(service: service)
     }
 
     func serviceOnboarding(didOnboardService service: Service) {
@@ -86,7 +86,7 @@ extension Settings.StateModel: ServiceOnboardingDelegate {
 
 extension Settings.StateModel: CompletionDelegate {
     func completionNotifyingDidComplete(_: CompletionNotifying) {
-        setupTidePool = false
-        provider.tidePoolManager.forceUploadData(device: fetchCgmManager.cgmManager?.cgmManagerStatus.device)
+        setupTidepool = false
+        provider.tidepoolManager.forceUploadData(device: fetchCgmManager.cgmManager?.cgmManagerStatus.device)
     }
 }

+ 3 - 24
FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift

@@ -29,10 +29,9 @@ extension Settings {
                 Section {
                     Text("Nightscout").navigationLink(to: .nighscoutConfig, from: self)
 
-                    Text("TidePool")
-                        .onTapGesture {
-                            state.setupTidePool = true
-                        }
+                    NavigationLink(destination: TidepoolStartView(state: state)) {
+                        Text("Tidepool")
+                    }
                     if HKHealthStore.isHealthDataAvailable() {
                         Text("Apple Health").navigationLink(to: .healthkit, from: self)
                     }
@@ -130,26 +129,6 @@ extension Settings {
             .sheet(isPresented: $showShareSheet) {
                 ShareSheet(activityItems: state.logItems())
             }
-            .sheet(isPresented: $state.setupTidePool) {
-                if let serviceUIType = state.serviceUIType,
-                   let pluginHost = state.provider.tidePoolManager.getTidePoolPluginHost()
-                {
-                    if let serviceUI = state.provider.tidePoolManager.getTidePoolServiceUI() {
-                        TidePoolSettingsView(
-                            serviceUI: serviceUI,
-                            serviceOnBoardDelegate: self.state,
-                            serviceDelegate: self.state
-                        )
-                    } else {
-                        TidePoolSetupView(
-                            serviceUIType: serviceUIType,
-                            pluginHost: pluginHost,
-                            serviceOnBoardDelegate: self.state,
-                            serviceDelegate: self.state
-                        )
-                    }
-                }
-            }
             .onAppear(perform: configureView)
             .navigationTitle("Settings")
             .navigationBarItems(leading: Button("Close", action: state.hideSettingsModal))

+ 6 - 6
FreeAPS/Sources/Modules/Settings/View/TidePoolConfigView.swift

@@ -3,13 +3,13 @@ import LoopKit
 import LoopKitUI
 import SwiftUI
 
-struct TidePoolSetupView: UIViewControllerRepresentable {
+struct TidepoolSetupView: UIViewControllerRepresentable {
     let serviceUIType: ServiceUI.Type
     let pluginHost: PluginHost
     let serviceOnBoardDelegate: ServiceOnboardingDelegate
     let serviceDelegate: CompletionDelegate
 
-    func makeUIViewController(context _: UIViewControllerRepresentableContext<TidePoolSetupView>) -> UIViewController {
+    func makeUIViewController(context _: UIViewControllerRepresentableContext<TidepoolSetupView>) -> UIViewController {
         let result = serviceUIType.setupViewController(
             colorPalette: .default,
             pluginHost: pluginHost
@@ -26,20 +26,20 @@ struct TidePoolSetupView: UIViewControllerRepresentable {
         }
     }
 
-    func updateUIViewController(_: UIViewController, context _: UIViewControllerRepresentableContext<TidePoolSetupView>) {}
+    func updateUIViewController(_: UIViewController, context _: UIViewControllerRepresentableContext<TidepoolSetupView>) {}
 }
 
-struct TidePoolSettingsView: UIViewControllerRepresentable {
+struct TidepoolSettingsView: UIViewControllerRepresentable {
     let serviceUI: ServiceUI
     let serviceOnBoardDelegate: ServiceOnboardingDelegate
     let serviceDelegate: CompletionDelegate?
 
-    func makeUIViewController(context _: UIViewControllerRepresentableContext<TidePoolSettingsView>) -> UIViewController {
+    func makeUIViewController(context _: UIViewControllerRepresentableContext<TidepoolSettingsView>) -> UIViewController {
         var vc = serviceUI.settingsViewController(colorPalette: .default)
         vc.completionDelegate = serviceDelegate
         vc.serviceOnboardingDelegate = serviceOnBoardDelegate
         return vc
     }
 
-    func updateUIViewController(_: UIViewController, context _: UIViewControllerRepresentableContext<TidePoolSettingsView>) {}
+    func updateUIViewController(_: UIViewController, context _: UIViewControllerRepresentableContext<TidepoolSettingsView>) {}
 }

+ 46 - 0
FreeAPS/Sources/Modules/Settings/View/TidepoolStartView.swift

@@ -0,0 +1,46 @@
+
+import SwiftUI
+
+struct TidepoolStartView: View {
+    @ObservedObject var state: Settings.StateModel
+
+    var body: some View {
+        Form {
+            Section(
+                header: Text("Connect to Tidepool"),
+                footer: VStack(alignment: .leading, spacing: 2) {
+                    Text(
+                        "When connected, uploading of carbs, bolus, basal and glucose from Trio to your Tidepool account is enabled."
+                    )
+                    Text(
+                        "\nUse your Tidepool credentials to login. If you dont already have a Tidepool account, you can sign up for one on the login page."
+                    )
+                }
+            )
+                {
+                    Button("Connect to Tidepool") { state.setupTidepool = true }
+                }
+                .navigationTitle("Tidepool")
+        }
+        .sheet(isPresented: $state.setupTidepool) {
+            if let serviceUIType = state.serviceUIType,
+               let pluginHost = state.provider.tidepoolManager.getTidepoolPluginHost()
+            {
+                if let serviceUI = state.provider.tidepoolManager.getTidepoolServiceUI() {
+                    TidepoolSettingsView(
+                        serviceUI: serviceUI,
+                        serviceOnBoardDelegate: self.state,
+                        serviceDelegate: self.state
+                    )
+                } else {
+                    TidepoolSetupView(
+                        serviceUIType: serviceUIType,
+                        pluginHost: pluginHost,
+                        serviceOnBoardDelegate: self.state,
+                        serviceDelegate: self.state
+                    )
+                }
+            }
+        }
+    }
+}

+ 7 - 3
FreeAPS/Sources/Services/Network/NightscoutManager.swift

@@ -44,6 +44,10 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         settingsManager.settings.isUploadEnabled
     }
 
+    private var isDownloadEnabled: Bool {
+        settingsManager.settings.isDownloadEnabled
+    }
+
     private var isUploadGlucoseEnabled: Bool {
         settingsManager.settings.uploadGlucose
     }
@@ -140,7 +144,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     }
 
     func fetchCarbs() -> AnyPublisher<[CarbsEntry], Never> {
-        guard let nightscout = nightscoutAPI, isNetworkReachable else {
+        guard let nightscout = nightscoutAPI, isNetworkReachable, isDownloadEnabled else {
             return Just([]).eraseToAnyPublisher()
         }
 
@@ -151,7 +155,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     }
 
     func fetchTempTargets() -> AnyPublisher<[TempTarget], Never> {
-        guard let nightscout = nightscoutAPI, isNetworkReachable else {
+        guard let nightscout = nightscoutAPI, isNetworkReachable, isDownloadEnabled else {
             return Just([]).eraseToAnyPublisher()
         }
 
@@ -162,7 +166,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     }
 
     func fetchAnnouncements() -> AnyPublisher<[Announcement], Never> {
-        guard let nightscout = nightscoutAPI, isNetworkReachable else {
+        guard let nightscout = nightscoutAPI, isNetworkReachable, isDownloadEnabled else {
             return Just([]).eraseToAnyPublisher()
         }
 

+ 47 - 47
FreeAPS/Sources/Services/Network/TidepoolManager.swift

@@ -5,10 +5,10 @@ import LoopKit
 import LoopKitUI
 import Swinject
 
-protocol TidePoolManager {
-    func addTidePoolService(service: Service)
-    func getTidePoolServiceUI() -> ServiceUI?
-    func getTidePoolPluginHost() -> PluginHost?
+protocol TidepoolManager {
+    func addTidepoolService(service: Service)
+    func getTidepoolServiceUI() -> ServiceUI?
+    func getTidepoolPluginHost() -> PluginHost?
     func deleteCarbs(at date: Date, isFPU: Bool?, fpuID: String?, syncID: String)
     func deleteInsulin(at date: Date)
 //    func uploadStatus()
@@ -18,7 +18,7 @@ protocol TidePoolManager {
 //    func uploadProfileAndSettings(_: Bool)
 }
 
-final class BaseTidePoolManager: TidePoolManager, Injectable {
+final class BaseTidepoolManager: TidepoolManager, Injectable {
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var pluginManager: PluginManager!
     @Injected() private var glucoseStorage: GlucoseStorage!
@@ -27,53 +27,53 @@ final class BaseTidePoolManager: TidePoolManager, Injectable {
     @Injected() private var pumpHistoryStorage: PumpHistoryStorage!
 
     private let processQueue = DispatchQueue(label: "BaseNetworkManager.processQueue")
-    private var tidePoolService: RemoteDataService? {
+    private var tidepoolService: RemoteDataService? {
         didSet {
-            if let tidePoolService = tidePoolService {
-                rawTidePoolManager = tidePoolService.rawValue
+            if let tidepoolService = tidepoolService {
+                rawTidepoolManager = tidepoolService.rawValue
             } else {
-                rawTidePoolManager = nil
+                rawTidepoolManager = nil
             }
         }
     }
 
-    @PersistedProperty(key: "TidePoolState") var rawTidePoolManager: Service.RawValue?
+    @PersistedProperty(key: "TidepoolState") var rawTidepoolManager: Service.RawValue?
 
     init(resolver: Resolver) {
         injectServices(resolver)
-        loadTidePoolManager()
+        loadTidepoolManager()
         subscribe()
     }
 
-    /// load the TidePool Remote Data Service if available
-    fileprivate func loadTidePoolManager() {
-        if let rawTidePoolManager = rawTidePoolManager {
-            tidePoolService = tidePoolServiceFromRaw(rawTidePoolManager)
-            tidePoolService?.serviceDelegate = self
-            tidePoolService?.stateDelegate = self
+    /// load the Tidepool Remote Data Service if available
+    fileprivate func loadTidepoolManager() {
+        if let rawTidepoolManager = rawTidepoolManager {
+            tidepoolService = tidepoolServiceFromRaw(rawTidepoolManager)
+            tidepoolService?.serviceDelegate = self
+            tidepoolService?.stateDelegate = self
         }
     }
 
-    /// allows to acces to tidePoolService as a simple ServiceUI
-    func getTidePoolServiceUI() -> ServiceUI? {
-        if let tidePoolService = self.tidePoolService {
-            return tidePoolService as! any ServiceUI as ServiceUI
+    /// allows access to tidepoolService as a simple ServiceUI
+    func getTidepoolServiceUI() -> ServiceUI? {
+        if let tidepoolService = self.tidepoolService {
+            return tidepoolService as! any ServiceUI as ServiceUI
         } else {
             return nil
         }
     }
 
-    /// get the pluginHost of TidePool
-    func getTidePoolPluginHost() -> PluginHost? {
+    /// get the pluginHost of Tidepool
+    func getTidepoolPluginHost() -> PluginHost? {
         self as PluginHost
     }
 
-    func addTidePoolService(service: Service) {
-        tidePoolService = service as! any RemoteDataService as RemoteDataService
+    func addTidepoolService(service: Service) {
+        tidepoolService = service as! any RemoteDataService as RemoteDataService
     }
 
-    /// load the TidePool Remote Data Service from raw storage
-    private func tidePoolServiceFromRaw(_ rawValue: [String: Any]) -> RemoteDataService? {
+    /// load the Tidepool Remote Data Service from raw storage
+    private func tidepoolServiceFromRaw(_ rawValue: [String: Any]) -> RemoteDataService? {
         guard let rawState = rawValue["state"] as? Service.RawStateValue,
               let serviceType = pluginManager.getServiceTypeByIdentifier("TidepoolService")
         else {
@@ -97,15 +97,15 @@ final class BaseTidePoolManager: TidePoolManager, Injectable {
     func uploadCarbs() {
         let carbs: [CarbsEntry] = carbsStorage.recent()
 
-        guard !carbs.isEmpty, let tidePoolService = self.tidePoolService else { return }
+        guard !carbs.isEmpty, let tidepoolService = self.tidepoolService else { return }
 
         processQueue.async {
-            carbs.chunks(ofCount: tidePoolService.carbDataLimit ?? 100).forEach { chunk in
+            carbs.chunks(ofCount: tidepoolService.carbDataLimit ?? 100).forEach { chunk in
 
                 let syncCarb: [SyncCarbObject] = Array(chunk).map {
                     $0.convertSyncCarb()
                 }
-                tidePoolService.uploadCarbData(created: syncCarb, updated: [], deleted: []) { result in
+                tidepoolService.uploadCarbData(created: syncCarb, updated: [], deleted: []) { result in
                     switch result {
                     case let .failure(error):
                         debug(.nightscout, "Error synchronizing carbs data: \(String(describing: error))")
@@ -118,7 +118,7 @@ final class BaseTidePoolManager: TidePoolManager, Injectable {
     }
 
     func deleteCarbs(at date: Date, isFPU: Bool?, fpuID: String?, syncID _: String) {
-        guard let tidePoolService = self.tidePoolService else { return }
+        guard let tidepoolService = self.tidepoolService else { return }
 
         processQueue.async {
             var carbsToDelete: [CarbsEntry] = []
@@ -135,7 +135,7 @@ final class BaseTidePoolManager: TidePoolManager, Injectable {
                 d.convertSyncCarb(operation: .delete)
             }
 
-            tidePoolService.uploadCarbData(created: [], updated: [], deleted: syncCarb) { result in
+            tidepoolService.uploadCarbData(created: [], updated: [], deleted: syncCarb) { result in
                 switch result {
                 case let .failure(error):
                     debug(.nightscout, "Error synchronizing carbs data: \(String(describing: error))")
@@ -149,7 +149,7 @@ final class BaseTidePoolManager: TidePoolManager, Injectable {
     func deleteInsulin(at d: Date) {
         let allValues = storage.retrieve(OpenAPS.Monitor.pumpHistory, as: [PumpHistoryEvent].self) ?? []
 
-        guard !allValues.isEmpty, let tidePoolService = self.tidePoolService else { return }
+        guard !allValues.isEmpty, let tidepoolService = self.tidepoolService else { return }
 
         var doseDataToDelete: [DoseEntry] = []
 
@@ -166,7 +166,7 @@ final class BaseTidePoolManager: TidePoolManager, Injectable {
             ))
 
         processQueue.async {
-            tidePoolService.uploadDoseData(created: [], deleted: doseDataToDelete) { result in
+            tidepoolService.uploadDoseData(created: [], deleted: doseDataToDelete) { result in
                 switch result {
                 case let .failure(error):
                     debug(.nightscout, "Error synchronizing Dose delete data: \(String(describing: error))")
@@ -179,7 +179,7 @@ final class BaseTidePoolManager: TidePoolManager, Injectable {
 
     func uploadDose() {
         let events = pumpHistoryStorage.recent()
-        guard !events.isEmpty, let tidePoolService = self.tidePoolService else { return }
+        guard !events.isEmpty, let tidepoolService = self.tidepoolService else { return }
 
         let eventsBasal = events.filter { $0.type == .tempBasal || $0.type == .tempBasalDuration }
             .sorted { $0.timestamp < $1.timestamp }
@@ -297,7 +297,7 @@ final class BaseTidePoolManager: TidePoolManager, Injectable {
         }
 
         processQueue.async {
-            tidePoolService.uploadDoseData(created: doseDataBasal + boluses, deleted: []) { result in
+            tidepoolService.uploadDoseData(created: doseDataBasal + boluses, deleted: []) { result in
                 switch result {
                 case let .failure(error):
                     debug(.nightscout, "Error synchronizing Dose data: \(String(describing: error))")
@@ -306,7 +306,7 @@ final class BaseTidePoolManager: TidePoolManager, Injectable {
                 }
             }
 
-            tidePoolService.uploadPumpEventData(pumpEvents) { result in
+            tidepoolService.uploadPumpEventData(pumpEvents) { result in
                 switch result {
                 case let .failure(error):
                     debug(.nightscout, "Error synchronizing Pump Event data: \(String(describing: error))")
@@ -320,19 +320,19 @@ final class BaseTidePoolManager: TidePoolManager, Injectable {
     func uploadGlucose(device: HKDevice?) {
         let glucose: [BloodGlucose] = glucoseStorage.recent()
 
-        guard !glucose.isEmpty, let tidePoolService = self.tidePoolService else { return }
+        guard !glucose.isEmpty, let tidepoolService = self.tidepoolService else { return }
 
         let glucoseWithoutCorrectID = glucose.filter { UUID(uuidString: $0._id) != nil }
 
         processQueue.async {
-            glucoseWithoutCorrectID.chunks(ofCount: tidePoolService.glucoseDataLimit ?? 100)
+            glucoseWithoutCorrectID.chunks(ofCount: tidepoolService.glucoseDataLimit ?? 100)
                 .forEach { chunk in
                     // all glucose attached with the current device ;-(
 
                     let chunkStoreGlucose = Array(chunk).map {
                         $0.convertStoredGlucoseSample(device: device)
                     }
-                    tidePoolService.uploadGlucoseData(chunkStoreGlucose) { result in
+                    tidepoolService.uploadGlucoseData(chunkStoreGlucose) { result in
                         switch result {
                         case let .failure(error):
                             debug(.nightscout, "Error synchronizing glucose data: \(String(describing: error))")
@@ -345,7 +345,7 @@ final class BaseTidePoolManager: TidePoolManager, Injectable {
         }
     }
 
-    /// force to uploads all data in TidePool Service
+    /// force to uploads all data in Tidepool Service
     func forceUploadData(device: HKDevice?) {
         uploadDose()
         uploadCarbs()
@@ -353,23 +353,23 @@ final class BaseTidePoolManager: TidePoolManager, Injectable {
     }
 }
 
-extension BaseTidePoolManager: PumpHistoryObserver {
+extension BaseTidepoolManager: PumpHistoryObserver {
     func pumpHistoryDidUpdate(_: [PumpHistoryEvent]) {
         uploadDose()
     }
 }
 
-extension BaseTidePoolManager: CarbsObserver {
+extension BaseTidepoolManager: CarbsObserver {
     func carbsDidUpdate(_: [CarbsEntry]) {
         uploadCarbs()
     }
 }
 
-extension BaseTidePoolManager: TempTargetsObserver {
+extension BaseTidepoolManager: TempTargetsObserver {
     func tempTargetsDidUpdate(_: [TempTarget]) {}
 }
 
-extension BaseTidePoolManager: ServiceDelegate {
+extension BaseTidepoolManager: ServiceDelegate {
     var hostIdentifier: String {
         "com.loopkit.Loop" // To check
     }
@@ -404,11 +404,11 @@ extension BaseTidePoolManager: ServiceDelegate {
     func deliverRemoteBolus(amountInUnits _: Double) async throws {}
 }
 
-extension BaseTidePoolManager: StatefulPluggableDelegate {
+extension BaseTidepoolManager: StatefulPluggableDelegate {
     func pluginDidUpdateState(_: LoopKit.StatefulPluggable) {}
 
     func pluginWantsDeletion(_: LoopKit.StatefulPluggable) {
-        tidePoolService = nil
+        tidepoolService = nil
     }
 }
 

+ 9 - 5
FreeAPSTests/CalibrationsTests.swift

@@ -12,6 +12,9 @@ class CalibrationsTests: XCTestCase, Injectable {
     }
 
     func testCreateSimpleCalibration() {
+        // restore state so each test is independent
+        calibrationService.removeAllCalibrations()
+
         let calibration = Calibration(x: 100.0, y: 102.0)
         calibrationService.addCalibration(calibration)
 
@@ -25,17 +28,18 @@ class CalibrationsTests: XCTestCase, Injectable {
     }
 
     func testCreateMultipleCalibration() {
+        // restore state so each test is independent
+        calibrationService.removeAllCalibrations()
+
         let calibration = Calibration(x: 100.0, y: 120)
         calibrationService.addCalibration(calibration)
 
         let calibration2 = Calibration(x: 120.0, y: 130.0)
         calibrationService.addCalibration(calibration2)
 
-        XCTAssertTrue(calibrationService.slope == 0.8)
-
-        XCTAssertTrue(calibrationService.intercept == 37)
-
-        XCTAssertTrue(calibrationService.calibrate(value: 80) == 101)
+        XCTAssertEqual(calibrationService.slope, 0.8, accuracy: 0.0001)
+        XCTAssertEqual(calibrationService.intercept, 37, accuracy: 0.0001)
+        XCTAssertEqual(calibrationService.calibrate(value: 80), 101, accuracy: 0.0001)
 
         calibrationService.removeLast()
 

+ 1 - 1
G7SensorKit

@@ -1 +1 @@
-Subproject commit 50515639919465e54e485ab29fd395b0eafd76e3
+Subproject commit b5e992e211d2ac6224acb105dd97fb484767da72