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

Merge branch 'nightscout:dev' into patch-1

Liroy van Hoewijk 1 год назад
Родитель
Сommit
7a093ff83f
93 измененных файлов с 2249 добавлено и 921 удалено
  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. 1 1
      .github/workflows/build_trio.yml
  5. 20 4
      FreeAPS.xcodeproj/project.pbxproj
  6. 8 8
      FreeAPS.xcodeproj/xcshareddata/xcschemes/Trio.xcscheme
  7. 1 1
      FreeAPS/Resources/javascript/bundle/autosens.js
  8. 1 1
      FreeAPS/Resources/javascript/bundle/autotune-prep.js
  9. 1 1
      FreeAPS/Resources/javascript/bundle/iob.js
  10. 1 1
      FreeAPS/Resources/javascript/bundle/meal.js
  11. 1 1
      FreeAPS/Resources/javascript/bundle/profile.js
  12. 4 0
      FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json
  13. 1 1
      FreeAPS/Resources/json/defaults/preferences.json
  14. 17 49
      FreeAPS/Sources/APS/DeviceDataManager.swift
  15. 2 16
      FreeAPS/Sources/APS/Extensions/PumpManagerExtensions.swift
  16. 19 10
      FreeAPS/Sources/APS/Extensions/UserDefaultsExtensions.swift
  17. 3 2
      FreeAPS/Sources/APS/FetchGlucoseManager.swift
  18. 118 73
      FreeAPS/Sources/APS/Storage/CarbsStorage.swift
  19. 2 2
      FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift
  20. 1 1
      FreeAPS/Sources/Assemblies/NetworkAssembly.swift
  21. 0 1
      FreeAPS/Sources/Helpers/PropertyWrappers/PersistedProperty.swift
  22. 29 8
      FreeAPS/Sources/Localizations/Main/ar.lproj/Localizable.strings
  23. 32 5
      FreeAPS/Sources/Localizations/Main/ca.lproj/Localizable.strings
  24. 29 8
      FreeAPS/Sources/Localizations/Main/da.lproj/Localizable.strings
  25. 29 8
      FreeAPS/Sources/Localizations/Main/de.lproj/Localizable.strings
  26. 29 8
      FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings
  27. 29 8
      FreeAPS/Sources/Localizations/Main/es.lproj/Localizable.strings
  28. 29 8
      FreeAPS/Sources/Localizations/Main/fi.lproj/Localizable.strings
  29. 29 8
      FreeAPS/Sources/Localizations/Main/fr.lproj/Localizable.strings
  30. 29 8
      FreeAPS/Sources/Localizations/Main/he.lproj/Localizable.strings
  31. 29 8
      FreeAPS/Sources/Localizations/Main/hu.lproj/Localizable.strings
  32. 29 8
      FreeAPS/Sources/Localizations/Main/it.lproj/Localizable.strings
  33. 29 8
      FreeAPS/Sources/Localizations/Main/nb.lproj/Localizable.strings
  34. 29 8
      FreeAPS/Sources/Localizations/Main/nl.lproj/Localizable.strings
  35. 29 8
      FreeAPS/Sources/Localizations/Main/pl.lproj/Localizable.strings
  36. 29 8
      FreeAPS/Sources/Localizations/Main/pt-BR.lproj/Localizable.strings
  37. 29 8
      FreeAPS/Sources/Localizations/Main/pt-PT.lproj/Localizable.strings
  38. 29 8
      FreeAPS/Sources/Localizations/Main/ru.lproj/Localizable.strings
  39. 29 8
      FreeAPS/Sources/Localizations/Main/sk.lproj/Localizable.strings
  40. 29 8
      FreeAPS/Sources/Localizations/Main/sv.lproj/Localizable.strings
  41. 29 8
      FreeAPS/Sources/Localizations/Main/tr.lproj/Localizable.strings
  42. 29 8
      FreeAPS/Sources/Localizations/Main/uk.lproj/Localizable.strings
  43. 29 8
      FreeAPS/Sources/Localizations/Main/vi.lproj/Localizable.strings
  44. 30 9
      FreeAPS/Sources/Localizations/Main/zh-Hans.lproj/Localizable.strings
  45. 20 0
      FreeAPS/Sources/Models/FreeAPSSettings.swift
  46. 8 0
      FreeAPS/Sources/Models/PumpHistoryEvent.swift
  47. 1 5
      FreeAPS/Sources/Models/RawFetchedProfile.swift
  48. 17 1
      FreeAPS/Sources/Modules/AddCarbs/AddCarbsStateModel.swift
  49. 28 14
      FreeAPS/Sources/Modules/AddCarbs/View/AddCarbsRootView.swift
  50. 49 2
      FreeAPS/Sources/Modules/AddTempTarget/AddTempTargetStateModel.swift
  51. 199 109
      FreeAPS/Sources/Modules/AddTempTarget/View/AddTempTargetRootView.swift
  52. 11 7
      FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift
  53. 5 5
      FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift
  54. 4 0
      FreeAPS/Sources/Modules/FPUConfig/FPUConfigStateModel.swift
  55. 9 1
      FreeAPS/Sources/Modules/FPUConfig/View/FPUConfigRootView.swift
  56. 30 1
      FreeAPS/Sources/Modules/Home/HomeStateModel.swift
  57. 105 41
      FreeAPS/Sources/Modules/Home/View/Header/PumpView.swift
  58. 3 4
      FreeAPS/Sources/Modules/Home/View/HomeRootView.swift
  59. 34 4
      FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift
  60. 42 90
      FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutConfigRootView.swift
  61. 58 0
      FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutConnectView.swift
  62. 41 0
      FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutFetchView.swift
  63. 26 0
      FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutUploadView.swift
  64. 116 1
      FreeAPS/Sources/Modules/OverrideProfilesConfig/OverrideProfilesStateModel.swift
  65. 271 128
      FreeAPS/Sources/Modules/OverrideProfilesConfig/View/OverrideProfilesRootView.swift
  66. 20 4
      FreeAPS/Sources/Modules/PreferencesEditor/PreferencesEditorDataFlow.swift
  67. 3 5
      FreeAPS/Sources/Modules/PreferencesEditor/PreferencesEditorStateModel.swift
  68. 0 3
      FreeAPS/Sources/Modules/PreferencesEditor/View/PreferencesEditorRootView.swift
  69. 2 0
      FreeAPS/Sources/Modules/PumpConfig/View/PumpConfigRootView.swift
  70. 17 4
      FreeAPS/Sources/Modules/PumpSettingsEditor/PumpSettingsEditorProvider.swift
  71. 3 0
      FreeAPS/Sources/Modules/PumpSettingsEditor/PumpSettingsEditorStateModel.swift
  72. 1 1
      FreeAPS/Sources/Modules/Settings/SettingsProvider.swift
  73. 21 4
      FreeAPS/Sources/Modules/Settings/SettingsStateModel.swift
  74. 13 24
      FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift
  75. 6 6
      FreeAPS/Sources/Modules/Settings/View/TidePoolConfigView.swift
  76. 46 0
      FreeAPS/Sources/Modules/Settings/View/TidepoolStartView.swift
  77. 2 2
      FreeAPS/Sources/Modules/WatchConfig/View/WatchConfigRootView.swift
  78. 2 0
      FreeAPS/Sources/Modules/WatchConfig/WatchConfigStateModel.swift
  79. 4 1
      FreeAPS/Sources/Router/Screen.swift
  80. 7 3
      FreeAPS/Sources/Services/Network/NightscoutManager.swift
  81. 47 47
      FreeAPS/Sources/Services/Network/TidepoolManager.swift
  82. 1 0
      FreeAPS/Sources/Services/WatchManager/WatchManager.swift
  83. 9 5
      FreeAPSTests/CalibrationsTests.swift
  84. 1 0
      FreeAPSWatch WatchKit Extension/DataFlow.swift
  85. 1 1
      FreeAPSWatch WatchKit Extension/Views/BolusConfirmationView.swift
  86. 2 0
      FreeAPSWatch WatchKit Extension/WatchStateModel.swift
  87. 1 1
      G7SensorKit
  88. 1 1
      LiveActivity/LiveActivity.swift
  89. 1 1
      OmniBLE
  90. 1 1
      OmniKit
  91. 22 9
      README.md
  92. 6 1
      oref0_source_version.txt
  93. 1 1
      trio-oref/lib/profile/index.js

+ 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.*

+ 1 - 1
.github/workflows/build_trio.yml

@@ -262,7 +262,7 @@ jobs:
       # Upload Build artifacts
       - name: Upload build log, IPA and Symbol artifacts
         if: always()
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
           name: build-artifacts
           path: |

+ 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 - 1
FreeAPS/Resources/javascript/bundle/autosens.js


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
FreeAPS/Resources/javascript/bundle/autotune-prep.js


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
FreeAPS/Resources/javascript/bundle/iob.js


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
FreeAPS/Resources/javascript/bundle/meal.js


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
FreeAPS/Resources/javascript/bundle/profile.js


+ 4 - 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,
@@ -40,6 +41,9 @@
   "oneDimensionalGraph" : false,
   "rulerMarks" : true,
   "maxCarbs": 250,
+  "maxFat": 250,
+  "maxProtein": 250,
   "displayFatAndProteinOnWatch": false,
+  "confirmBolusFaster": false,
   "lockScreenView": "simple"
 }

+ 1 - 1
FreeAPS/Resources/json/defaults/preferences.json

@@ -49,6 +49,6 @@
   "tddAdjBasal" : false,
   "enableSMB_high_bg" : false,
   "enableSMB_high_bg_target" : 110,
-  "threshold_setting" : 65,
+  "threshold_setting" : 60,
   "updateInterval" : 20
 }

+ 17 - 49
FreeAPS/Sources/APS/DeviceDataManager.swift

@@ -43,10 +43,6 @@ private let staticPumpManagersByIdentifier: [String: PumpManagerUI.Type] = [
     MockPumpManager.pluginIdentifier: MockPumpManager.self
 ]
 
-// private let staticPumpManagersByIdentifier: [String: PumpManagerUI.Type] = staticPumpManagers.reduce(into: [:]) { map, Type in
-//    map[Type.managerIdentifier] = Type
-// }
-
 private let accessLock = NSRecursiveLock(label: "BaseDeviceDataManager.accessLock")
 
 final class BaseDeviceDataManager: DeviceDataManager, Injectable {
@@ -78,7 +74,8 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
         didSet {
             pumpManager?.pumpManagerDelegate = self
             pumpManager?.delegateQueue = processQueue
-            UserDefaults.standard.pumpManagerRawValue = pumpManager?.rawValue
+            rawPumpManager = pumpManager?.rawValue
+            UserDefaults.standard.clearLegacyPumpManagerRawValue()
             if let pumpManager = pumpManager {
                 pumpDisplayState.value = PumpDisplayState(name: pumpManager.localizedTitle, image: pumpManager.smallImage)
                 pumpName.send(pumpManager.localizedTitle)
@@ -105,6 +102,8 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
         }
     }
 
+    @PersistedProperty(key: "PumpManagerState") var rawPumpManager: PumpManager.RawValue?
+
     var bluetoothManager: BluetoothStateManager { bluetoothProvider }
 
     var hasBLEHeartbeat: Bool {
@@ -123,7 +122,11 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
     }
 
     func setupPumpManager() {
-        pumpManager = UserDefaults.standard.pumpManagerRawValue.flatMap { pumpManagerFromRawValue($0) }
+        if let pumpManagerRawValue = rawPumpManager ?? UserDefaults.standard.legacyPumpManagerRawValue {
+            pumpManager = pumpManagerFromRawValue(pumpManagerRawValue)
+        } else {
+            pumpManager = nil
+        }
     }
 
     func createBolusProgressReporter() -> DoseProgressReporter? {
@@ -163,20 +166,6 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
                 self.updateUpdateFinished(true)
             }
         }
-
-//        pumpUpdateCancellable = Future<Bool, Never> { [unowned self] promise in
-//            pumpUpdatePromise = promise
-//            debug(.deviceManager, "Waiting for pump update and loop recommendation")
-//            processQueue.safeSync {
-//                pumpManager.ensureCurrentPumpData { _ in
-//                    debug(.deviceManager, "Pump data updated.")
-//                }
-//            }
-//        }
-//        .timeout(30, scheduler: processQueue)
-//        .replaceError(with: false)
-//        .replaceEmpty(with: false)
-//        .sink(receiveValue: updateUpdateFinished)
     }
 
     private func updateUpdateFinished(_ recommendsLoop: Bool) {
@@ -186,11 +175,6 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
             warning(.deviceManager, "Loop recommendation time out or got error. Trying to loop right now.")
         }
 
-        // directly in loop() function
-//        guard !loopInProgress else {
-//            warning(.deviceManager, "Loop already in progress. Skip recommendation.")
-//            return
-//        }
         self.recommendsLoop.send()
     }
 
@@ -319,7 +303,7 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
     }
 
     func pumpManagerDidUpdateState(_ pumpManager: PumpManager) {
-        UserDefaults.standard.pumpManagerRawValue = pumpManager.rawValue
+        rawPumpManager = pumpManager.rawValue
         if self.pumpManager == nil, let newPumpManager = pumpManager as? PumpManagerUI {
             self.pumpManager = newPumpManager
         }
@@ -431,6 +415,9 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
     func pumpManagerWillDeactivate(_: PumpManager) {
         dispatchPrecondition(condition: .onQueue(processQueue))
         pumpManager = nil
+        broadcaster.notify(PumpDeactivatedObserver.self, on: processQueue) {
+            $0.pumpDeactivatedDidChange()
+        }
     }
 
     func pumpManager(_: PumpManager, didUpdatePumpRecordsBasalProfileStartEvents _: Bool) {}
@@ -537,29 +524,6 @@ extension BaseDeviceDataManager: DeviceManagerDelegate {
 
     func recordRetractedAlert(_: Alert, at _: Date) {}
 
-//    func scheduleNotification(
-//        for _: DeviceManager,
-//        identifier: String,
-//        content: UNNotificationContent,
-//        trigger: UNNotificationTrigger?
-//    ) {
-//        let request = UNNotificationRequest(
-//            identifier: identifier,
-//            content: content,
-//            trigger: trigger
-//        )
-//
-//        DispatchQueue.main.async {
-//            UNUserNotificationCenter.current().add(request)
-//        }
-//    }
-//
-//    func clearNotification(for _: DeviceManager, identifier: String) {
-//        DispatchQueue.main.async {
-//            UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [identifier])
-//        }
-//    }
-
     func removeNotificationRequests(for _: DeviceManager, identifiers: [String]) {
         DispatchQueue.main.async {
             UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: identifiers)
@@ -667,3 +631,7 @@ protocol PumpReservoirObserver {
 protocol PumpBatteryObserver {
     func pumpBatteryDidChange(_ battery: Battery)
 }
+
+protocol PumpDeactivatedObserver {
+    func pumpDeactivatedDidChange()
+}

+ 2 - 16
FreeAPS/Sources/APS/Extensions/PumpManagerExtensions.swift

@@ -2,6 +2,8 @@ import LoopKit
 import LoopKitUI
 
 extension PumpManager {
+    typealias RawValue = [String: Any]
+
     var rawValue: [String: Any] {
         [
             "managerIdentifier": pluginIdentifier, // "managerIdentifier": type(of: self).managerIdentifier,
@@ -11,14 +13,6 @@ extension PumpManager {
 }
 
 extension PumpManagerUI {
-//    static func setupViewController() -> PumpManagerSetupViewController & UIViewController & CompletionNotifying {
-//        setupViewController(
-//            insulinTintColor: .accentColor,
-//            guidanceColors: GuidanceColors(acceptable: .green, warning: .orange, critical: .red),
-//            allowedInsulinTypes: [.apidra, .humalog, .novolog, .fiasp, .lyumjev]
-//        )
-//    }
-
     func settingsViewController(
         bluetoothProvider: BluetoothProvider,
         pumpManagerOnboardingDelegate: PumpManagerOnboardingDelegate?
@@ -32,14 +26,6 @@ extension PumpManagerUI {
         vc.pumpManagerOnboardingDelegate = pumpManagerOnboardingDelegate
         return vc
     }
-
-//    func settingsViewController() -> UIViewController & CompletionNotifying {
-//        settingsViewController(
-//            insulinTintColor: .accentColor,
-//            guidanceColors: GuidanceColors(acceptable: .green, warning: .orange, critical: .red),
-//            allowedInsulinTypes: [.apidra, .humalog, .novolog, .fiasp, .lyumjev]
-//        )
-//    }
 }
 
 protocol PumpSettingsBuilder {

+ 19 - 10
FreeAPS/Sources/APS/Extensions/UserDefaultsExtensions.swift

@@ -5,17 +5,10 @@ import RileyLinkKit
 
 extension UserDefaults {
     private enum Key: String {
-        case pumpManagerRawValue = "com.rileylink.PumpManagerRawValue"
+        case legacyPumpManagerRawValue = "com.rileylink.PumpManagerRawValue"
         case rileyLinkConnectionManagerState = "com.rileylink.RileyLinkConnectionManagerState"
-    }
-
-    var pumpManagerRawValue: PumpManager.RawStateValue? {
-        get {
-            dictionary(forKey: Key.pumpManagerRawValue.rawValue)
-        }
-        set {
-            set(newValue, forKey: Key.pumpManagerRawValue.rawValue)
-        }
+        case legacyPumpManagerState = "com.loopkit.Loop.PumpManagerState"
+        case legacyCGMManagerState = "com.loopkit.Loop.CGMManagerState"
     }
 
     var rileyLinkConnectionManagerState: RileyLinkConnectionState? {
@@ -30,4 +23,20 @@ extension UserDefaults {
             set(newValue?.rawValue, forKey: Key.rileyLinkConnectionManagerState.rawValue)
         }
     }
+
+    var legacyPumpManagerRawValue: PumpManager.RawValue? {
+        dictionary(forKey: Key.legacyPumpManagerRawValue.rawValue)
+    }
+
+    func clearLegacyPumpManagerRawValue() {
+        set(nil, forKey: Key.legacyPumpManagerRawValue.rawValue)
+    }
+
+    var legacyCGMManagerRawValue: CGMManager.RawValue? {
+        dictionary(forKey: Key.legacyCGMManagerState.rawValue)
+    }
+
+    func clearLegacyCGMManagerRawValue() {
+        set(nil, forKey: Key.legacyCGMManagerState.rawValue)
+    }
 }

+ 3 - 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!
@@ -45,6 +45,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
     var cgmManager: CGMManagerUI? {
         didSet {
             rawCGMManager = cgmManager?.rawValue
+            UserDefaults.standard.clearLegacyCGMManagerRawValue()
         }
     }
 
@@ -235,7 +236,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) }
 

+ 118 - 73
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)
     }
@@ -187,8 +232,8 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
                 bolus: nil,
                 insulin: nil,
                 carbs: $0.carbs,
-                fat: nil,
-                protein: nil,
+                fat: $0.fat,
+                protein: $0.protein,
                 foodType: $0.note,
                 targetTop: nil,
                 targetBottom: nil

+ 2 - 2
FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift

@@ -332,8 +332,8 @@ extension NightscoutTreatment {
                 insulin: nil,
                 notes: nil,
                 carbs: Decimal(event.carbInput ?? 0),
-                fat: nil,
-                protein: nil,
+                fat: Decimal(event.fatInput ?? 0),
+                protein: Decimal(event.proteinInput ?? 0),
                 targetTop: nil,
                 targetBottom: nil
             )

+ 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) }
     }
 }

+ 0 - 1
FreeAPS/Sources/Helpers/PropertyWrappers/PersistedProperty.swift

@@ -57,7 +57,6 @@ import Foundation
         self.key = key
 
         let documents: URL
-
         guard let localDocuments = try? FileManager.default.url(
             for: .documentDirectory,
             in: .userDomainMask,

Разница между файлами не показана из-за своего большого размера
+ 29 - 8
FreeAPS/Sources/Localizations/Main/ar.lproj/Localizable.strings


Разница между файлами не показана из-за своего большого размера
+ 32 - 5
FreeAPS/Sources/Localizations/Main/ca.lproj/Localizable.strings


Разница между файлами не показана из-за своего большого размера
+ 29 - 8
FreeAPS/Sources/Localizations/Main/da.lproj/Localizable.strings


Разница между файлами не показана из-за своего большого размера
+ 29 - 8
FreeAPS/Sources/Localizations/Main/de.lproj/Localizable.strings


Разница между файлами не показана из-за своего большого размера
+ 29 - 8
FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings


Разница между файлами не показана из-за своего большого размера
+ 29 - 8
FreeAPS/Sources/Localizations/Main/es.lproj/Localizable.strings


Разница между файлами не показана из-за своего большого размера
+ 29 - 8
FreeAPS/Sources/Localizations/Main/fi.lproj/Localizable.strings


Разница между файлами не показана из-за своего большого размера
+ 29 - 8
FreeAPS/Sources/Localizations/Main/fr.lproj/Localizable.strings


Разница между файлами не показана из-за своего большого размера
+ 29 - 8
FreeAPS/Sources/Localizations/Main/he.lproj/Localizable.strings


Разница между файлами не показана из-за своего большого размера
+ 29 - 8
FreeAPS/Sources/Localizations/Main/hu.lproj/Localizable.strings


Разница между файлами не показана из-за своего большого размера
+ 29 - 8
FreeAPS/Sources/Localizations/Main/it.lproj/Localizable.strings


Разница между файлами не показана из-за своего большого размера
+ 29 - 8
FreeAPS/Sources/Localizations/Main/nb.lproj/Localizable.strings


Разница между файлами не показана из-за своего большого размера
+ 29 - 8
FreeAPS/Sources/Localizations/Main/nl.lproj/Localizable.strings


Разница между файлами не показана из-за своего большого размера
+ 29 - 8
FreeAPS/Sources/Localizations/Main/pl.lproj/Localizable.strings


Разница между файлами не показана из-за своего большого размера
+ 29 - 8
FreeAPS/Sources/Localizations/Main/pt-BR.lproj/Localizable.strings


Разница между файлами не показана из-за своего большого размера
+ 29 - 8
FreeAPS/Sources/Localizations/Main/pt-PT.lproj/Localizable.strings


Разница между файлами не показана из-за своего большого размера
+ 29 - 8
FreeAPS/Sources/Localizations/Main/ru.lproj/Localizable.strings


Разница между файлами не показана из-за своего большого размера
+ 29 - 8
FreeAPS/Sources/Localizations/Main/sk.lproj/Localizable.strings


Разница между файлами не показана из-за своего большого размера
+ 29 - 8
FreeAPS/Sources/Localizations/Main/sv.lproj/Localizable.strings


Разница между файлами не показана из-за своего большого размера
+ 29 - 8
FreeAPS/Sources/Localizations/Main/tr.lproj/Localizable.strings


Разница между файлами не показана из-за своего большого размера
+ 29 - 8
FreeAPS/Sources/Localizations/Main/uk.lproj/Localizable.strings


Разница между файлами не показана из-за своего большого размера
+ 29 - 8
FreeAPS/Sources/Localizations/Main/vi.lproj/Localizable.strings


Разница между файлами не показана из-за своего большого размера
+ 30 - 9
FreeAPS/Sources/Localizations/Main/zh-Hans.lproj/Localizable.strings


+ 20 - 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
@@ -41,7 +42,10 @@ struct FreeAPSSettings: JSON, Equatable {
     var oneDimensionalGraph: Bool = false
     var rulerMarks: Bool = true
     var maxCarbs: Decimal = 250
+    var maxFat: Decimal = 250
+    var maxProtein: Decimal = 250
     var displayFatAndProteinOnWatch: Bool = false
+    var confirmBolusFaster: Bool = false
     var onlyAutotuneBasals: Bool = false
     var useLiveActivity: Bool = false
     var lockScreenView: LockScreenView = .simple
@@ -73,6 +77,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
         }
@@ -218,10 +226,22 @@ extension FreeAPSSettings: Decodable {
             settings.maxCarbs = maxCarbs
         }
 
+        if let maxFat = try? container.decode(Decimal.self, forKey: .maxFat) {
+            settings.maxFat = maxFat
+        }
+
+        if let maxProtein = try? container.decode(Decimal.self, forKey: .maxProtein) {
+            settings.maxProtein = maxProtein
+        }
+
         if let displayFatAndProteinOnWatch = try? container.decode(Bool.self, forKey: .displayFatAndProteinOnWatch) {
             settings.displayFatAndProteinOnWatch = displayFatAndProteinOnWatch
         }
 
+        if let confirmBolusFaster = try? container.decode(Bool.self, forKey: .confirmBolusFaster) {
+            settings.confirmBolusFaster = confirmBolusFaster
+        }
+
         if let onlyAutotuneBasals = try? container.decode(Bool.self, forKey: .onlyAutotuneBasals) {
             settings.onlyAutotuneBasals = onlyAutotuneBasals
         }

+ 8 - 0
FreeAPS/Sources/Models/PumpHistoryEvent.swift

@@ -11,6 +11,8 @@ struct PumpHistoryEvent: JSON, Equatable {
     let rate: Decimal?
     let temp: TempType?
     let carbInput: Int?
+    let fatInput: Int?
+    let proteinInput: Int?
     let note: String?
     let isSMB: Bool?
     let isExternalInsulin: Bool?
@@ -25,6 +27,8 @@ struct PumpHistoryEvent: JSON, Equatable {
         rate: Decimal? = nil,
         temp: TempType? = nil,
         carbInput: Int? = nil,
+        fatInput: Int? = nil,
+        proteinInput: Int? = nil,
         note: String? = nil,
         isSMB: Bool? = nil,
         isExternalInsulin: Bool? = nil
@@ -38,6 +42,8 @@ struct PumpHistoryEvent: JSON, Equatable {
         self.rate = rate
         self.temp = temp
         self.carbInput = carbInput
+        self.fatInput = fatInput
+        self.proteinInput = proteinInput
         self.note = note
         self.isSMB = isSMB
         self.isExternalInsulin = isExternalInsulin
@@ -88,6 +94,8 @@ extension PumpHistoryEvent {
         case rate
         case temp
         case carbInput = "carb_input"
+        case fatInput
+        case proteinInput
         case note
         case isSMB
         case isExternalInsulin

+ 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]

+ 17 - 1
FreeAPS/Sources/Modules/AddCarbs/AddCarbsStateModel.swift

@@ -16,7 +16,9 @@ extension AddCarbs {
         @Published var dish: String = ""
         @Published var selection: Presets?
         @Published var summation: [String] = []
-        @Published var maxCarbs: Decimal = 0
+        @Published var maxCarbs: Decimal = 250
+        @Published var maxFat: Decimal = 250
+        @Published var maxProtein: Decimal = 250
         @Published var note: String = ""
 
         let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
@@ -25,6 +27,8 @@ extension AddCarbs {
             subscribeSetting(\.useFPUconversion, on: $useFPUconversion) { useFPUconversion = $0 }
             carbsRequired = provider.suggestion?.carbsReq
             maxCarbs = settings.settings.maxCarbs
+            maxFat = settings.settings.maxFat
+            maxProtein = settings.settings.maxProtein
         }
 
         func add() {
@@ -160,5 +164,17 @@ extension AddCarbs {
             }
             return waitersNotepadString
         }
+
+        func saveButtonText() -> String {
+            if carbs > maxCarbs {
+                return "\(NSLocalizedString("Max Carbs of", comment: "")) \(maxCarbs) \(NSLocalizedString("g", comment: "")) \(NSLocalizedString("exceeded", comment: ""))"
+            } else if fat > maxFat {
+                return "\(NSLocalizedString("Max Fat of", comment: "")) \(maxFat) \(NSLocalizedString("g", comment: "")) \(NSLocalizedString("exceeded", comment: ""))"
+            } else if protein > maxProtein {
+                return "\(NSLocalizedString("Max Protein of", comment: "")) \(maxProtein) \(NSLocalizedString("g", comment: "")) \(NSLocalizedString("exceeded", comment: ""))"
+            } else {
+                return NSLocalizedString("Save and continue", comment: "")
+            }
+        }
     }
 }

+ 28 - 14
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
 
@@ -48,7 +49,7 @@ extension AddCarbs {
                             autofocus: true,
                             cleanInput: true
                         )
-                        Text("grams").foregroundColor(.secondary)
+                        Text(state.carbs > state.maxCarbs ? "⚠️" : "g").foregroundColor(.secondary)
                     }.padding(.vertical)
 
                     if state.useFPUconversion {
@@ -57,7 +58,7 @@ extension AddCarbs {
                     HStack {
                         Text("Note").foregroundColor(.secondary)
                         TextField("", text: $state.note).multilineTextAlignment(.trailing)
-                        if state.note != "", isFocused {
+                        if isFocused {
                             Button { isFocused = false } label: { Image(systemName: "keyboard.chevron.compact.down") }
                                 .controlSize(.mini)
                         }
@@ -117,9 +118,23 @@ extension AddCarbs {
                 }
 
                 Section {
-                    Button { state.add() }
-                    label: { Text("Save and continue").font(.title3) }
-                        .disabled(state.carbs <= 0 && state.fat <= 0 && state.protein <= 0)
+                    Button {
+                        mealSaved = true
+                        state.add()
+                    }
+                    label: { Text(state.saveButtonText()).font(.title3) }
+                        .disabled(
+                            mealSaved
+                                || state.carbs > state.maxCarbs
+                                || state.fat > state.maxFat
+                                || state.protein > state.maxProtein
+                                || (state.carbs <= 0 && state.fat <= 0 && state.protein <= 0)
+                        )
+                        .foregroundStyle(
+                            mealSaved || (state.carbs <= 0 && state.fat <= 0 && state.protein <= 0) ? .gray :
+                                state.carbs > state.maxCarbs || state.fat > state.maxFat || state.protein > state
+                                .maxProtein ? .red : .blue
+                        )
                         .frame(maxWidth: .infinity, alignment: .center)
                 } footer: { Text(state.waitersNotepad().description) }
 
@@ -138,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
@@ -147,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") }
@@ -260,7 +275,7 @@ extension AddCarbs {
                     autofocus: false,
                     cleanInput: true
                 )
-                Text("grams").foregroundColor(.secondary)
+                Text(state.fat > state.maxFat ? "⚠️" : "g").foregroundColor(.secondary)
             }
             HStack {
                 Text("Protein").foregroundColor(.red) // .fontWeight(.thin)
@@ -271,9 +286,8 @@ extension AddCarbs {
                     formatter: formatter,
                     autofocus: false,
                     cleanInput: true
-                ).foregroundColor(.loopRed)
-
-                Text("grams").foregroundColor(.secondary)
+                )
+                Text(state.protein > state.maxProtein ? "⚠️" : "g").foregroundColor(.secondary)
             }
         }
     }

+ 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!
-                    }
             }
-        }
-    }
+        } }
 }

+ 11 - 7
FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift

@@ -72,7 +72,7 @@ extension Bolus {
                                 autofocus: true,
                                 cleanInput: true
                             )
-                            Text("U").foregroundColor(.secondary)
+                            Text(state.amount > state.maxBolus ? "⚠️" : "U").foregroundColor(.secondary)
                         }
                     }
                     header: { Text("Bolus") }
@@ -81,15 +81,19 @@ extension Bolus {
                         label: {
                             Text(
                                 state.amount <= state.maxBolus ? NSLocalizedString("Enact bolus", comment: "") :
-                                    NSLocalizedString("Max Bolus exceeded!", comment: "")
-                                    + " (>"
+                                    NSLocalizedString("Max Bolus of", comment: "")
+                                    + " "
                                     + formatter.string(from: state.maxBolus as NSNumber)!
                                     + NSLocalizedString("U", comment: "Insulin unit")
-                                    + ")"
-                            ) }
-                            .disabled(
-                                state.amount <= 0 || state.amount > state.maxBolus
+                                    + " "
+                                    + NSLocalizedString("exceeded", comment: "")
+                            ).font(.title3) }
+                            .disabled(state.amount <= 0 || state.amount > state.maxBolus)
+                            .foregroundStyle(
+                                state.amount <= 0 ? .gray :
+                                    state.amount > state.maxBolus ? .red : .blue
                             )
+                            .frame(maxWidth: .infinity, alignment: .center)
                     }
                     if waitForSuggestion {
                         Section {

+ 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)

+ 4 - 0
FreeAPS/Sources/Modules/FPUConfig/FPUConfigStateModel.swift

@@ -3,6 +3,8 @@ import SwiftUI
 extension FPUConfig {
     final class StateModel: BaseStateModel<Provider> {
         @Published var maxCarbs: Decimal = 250
+        @Published var maxFat: Decimal = 250
+        @Published var maxProtein: Decimal = 250
         @Published var individualAdjustmentFactor: Decimal = 0
         @Published var timeCap: Decimal = 0
         @Published var minuteInterval: Decimal = 0
@@ -10,6 +12,8 @@ extension FPUConfig {
 
         override func subscribe() {
             subscribeSetting(\.maxCarbs, on: $maxCarbs) { maxCarbs = $0 }
+            subscribeSetting(\.maxFat, on: $maxFat) { maxFat = $0 }
+            subscribeSetting(\.maxProtein, on: $maxProtein) { maxProtein = $0 }
             subscribeSetting(\.timeCap, on: $timeCap.map(Int.init), initial: {
                 let value = max(min($0, 12), 5)
                 timeCap = Decimal(value)

+ 9 - 1
FreeAPS/Sources/Modules/FPUConfig/View/FPUConfigRootView.swift

@@ -28,11 +28,19 @@ extension FPUConfig {
 
         var body: some View {
             Form {
-                Section(header: Text("Carbohydrate limit")) {
+                Section(header: Text("Limit Per Entry")) {
                     HStack {
                         Text("Max Carbs")
                         DecimalTextField("g", value: $state.maxCarbs, formatter: formatter)
                     }
+                    HStack {
+                        Text("Max Fat")
+                        DecimalTextField("g", value: $state.maxFat, formatter: formatter)
+                    }
+                    HStack {
+                        Text("Max Protein")
+                        DecimalTextField("g", value: $state.maxProtein, formatter: formatter)
+                    }
                 }
 
                 Section(header: Text("Fat and Protein Conversion Settings")) {

+ 30 - 1
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
 
@@ -106,6 +107,7 @@ extension Home {
             broadcaster.register(EnactedSuggestionObserver.self, observer: self)
             broadcaster.register(PumpBatteryObserver.self, observer: self)
             broadcaster.register(PumpReservoirObserver.self, observer: self)
+            broadcaster.register(PumpDeactivatedObserver.self, observer: self)
 
             animatedBackground = settingsManager.settings.animatedBackground
 
@@ -168,6 +170,7 @@ extension Home {
                     } else {
                         self.setupBattery()
                         self.setupReservoir()
+                        self.displayPumpStatusHighlightMessage()
                     }
                 }
                 .store(in: &lifetime)
@@ -185,6 +188,8 @@ extension Home {
                             setupDelegate: self
                         ).asAny()
                         self.router.mainSecondaryModalView.send(view)
+                    } else if show {
+                        self.router.mainSecondaryModalView.send(self.router.view(for: .pumpConfigDirect))
                     } else {
                         self.router.mainSecondaryModalView.send(nil)
                     }
@@ -346,6 +351,22 @@ 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(_ didDeactivate: Bool = false) {
+            DispatchQueue.main.async { [weak self] in
+                guard let self = self else { return }
+                if let statusHighlight = self.provider.deviceManager.pumpManager?.pumpStatusHighlight,
+                   statusHighlight.state == .warning || statusHighlight.state == .critical, !didDeactivate
+                {
+                    pumpStatusHighlightMessage = (statusHighlight.state == .warning ? "⚠️\n" : "‼️\n") + statusHighlight
+                        .localizedMessage
+                } else {
+                    pumpStatusHighlightMessage = nil
+                }
+            }
+        }
+
         private func setupCurrentTempTarget() {
             tempTarget = provider.tempTarget()
         }
@@ -376,7 +397,8 @@ extension Home.StateModel:
     CarbsObserver,
     EnactedSuggestionObserver,
     PumpBatteryObserver,
-    PumpReservoirObserver
+    PumpReservoirObserver,
+    PumpDeactivatedObserver
 {
     func glucoseDidUpdate(_: [BloodGlucose]) {
         setupGlucose()
@@ -410,6 +432,7 @@ extension Home.StateModel:
         setupBasals()
         setupBoluses()
         setupSuspensions()
+        displayPumpStatusHighlightMessage()
     }
 
     func pumpSettingsDidChange(_: PumpSettings) {
@@ -435,10 +458,16 @@ extension Home.StateModel:
 
     func pumpBatteryDidChange(_: Battery) {
         setupBattery()
+        displayPumpStatusHighlightMessage()
     }
 
     func pumpReservoirDidChange(_: Decimal) {
         setupReservoir()
+        displayPumpStatusHighlightMessage()
+    }
+
+    func pumpDeactivatedDidChange() {
+        displayPumpStatusHighlightMessage(true)
     }
 }
 

+ 105 - 41
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,48 +22,67 @@ struct PumpView: View {
     }
 
     var body: some View {
-        VStack(alignment: .leading, spacing: 12) {
-            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)
+        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 {
+                        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)
-            }
-
-            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)
+                    }.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)
+                }
             }
         }
     }
@@ -138,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)
+    )
+}

+ 3 - 4
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -146,12 +146,11 @@ extension Home {
                 battery: $state.battery,
                 name: $state.pumpName,
                 expiresAtDate: $state.pumpExpiresAtDate,
-                timerDate: $state.timerDate
+                timerDate: $state.timerDate,
+                pumpStatusHighlightMessage: $state.pumpStatusHighlightMessage
             )
             .onTapGesture {
-                if state.pumpDisplayState != nil {
-                    state.setupPump = true
-                }
+                state.setupPump = true
             }
         }
 

+ 34 - 4
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 })
@@ -86,6 +88,24 @@ extension NightscoutConfig {
             return NightscoutAPI(url: url, secret: secret)
         }
 
+        private func getMedianTarget(
+            lowTargetValue: Decimal,
+            lowTargetTime: String,
+            highTarget: [NightscoutTimevalue],
+            units: GlucoseUnits
+        ) -> Decimal {
+            if let idx = highTarget.firstIndex(where: { $0.time == lowTargetTime }) {
+                let median = (lowTargetValue + highTarget[idx].value) / 2
+                switch units {
+                case .mgdL:
+                    return Decimal(round(Double(median)))
+                case .mmolL:
+                    return Decimal(round(Double(median) * 10) / 10)
+                }
+            }
+            return lowTargetValue
+        }
+
         func importSettings() {
             guard let nightscout = nightscoutAPI else {
                 saveError("Can't access nightscoutAPI")
@@ -135,7 +155,11 @@ 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["default"] != nil) ?
+                            fetchedProfileStore.first?.store["default"] :
+                            fetchedProfileStore.first?.store["Default"]
                         else {
                             error = "\nCan't find the default Nightscout Profile."
                             group.leave()
@@ -220,9 +244,15 @@ extension NightscoutConfig {
 
                         let targets = fetchedProfile.target_low
                             .map { target -> BGTargetEntry in
-                                BGTargetEntry(
-                                    low: target.value,
-                                    high: target.value,
+                                let median = loop! ? self.getMedianTarget(
+                                    lowTargetValue: target.value,
+                                    lowTargetTime: target.time,
+                                    highTarget: fetchedProfile.target_high,
+                                    units: self.units
+                                ) : target.value
+                                return BGTargetEntry(
+                                    low: median,
+                                    high: median,
                                     start: target.time,
                                     offset: self.offset(target.time) / 60
                                 ) }

+ 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]

+ 20 - 4
FreeAPS/Sources/Modules/PreferencesEditor/PreferencesEditorDataFlow.swift

@@ -11,7 +11,11 @@ enum PreferencesEditor {
 
     enum FieldType {
         case boolean(keypath: WritableKeyPath<Preferences, Bool>)
-        case decimal(keypath: WritableKeyPath<Preferences, Decimal>)
+        case decimal(
+            keypath: WritableKeyPath<Preferences, Decimal>,
+            minVal: WritableKeyPath<Preferences, Decimal>? = nil,
+            maxVal: WritableKeyPath<Preferences, Decimal>? = nil
+        )
         case insulinCurve(keypath: WritableKeyPath<Preferences, InsulinCurve>)
     }
 
@@ -34,7 +38,7 @@ enum PreferencesEditor {
         var decimalValue: Decimal {
             get {
                 switch type {
-                case let .decimal(keypath):
+                case let .decimal(keypath, _, _):
                     return settable?.get(keypath) ?? 0
                 default: return 0
                 }
@@ -57,8 +61,20 @@ enum PreferencesEditor {
             switch (type, value) {
             case let (.boolean(keypath), value as Bool):
                 settable?.set(keypath, value: value)
-            case let (.decimal(keypath), value as Decimal):
-                settable?.set(keypath, value: value)
+            case let (.decimal(keypath, minVal, maxVal), value as Decimal):
+                let constrainedValue: Decimal
+                if let minValue = minVal, let minValueDecimal: Decimal = settable?.get(minValue), let maxValue = maxVal,
+                   let maxValueDecimal: Decimal = settable?.get(maxValue)
+                {
+                    constrainedValue = min(max(value, minValueDecimal), maxValueDecimal)
+                } else if let minValue = minVal, let minValueDecimal: Decimal = settable?.get(minValue) {
+                    constrainedValue = max(value, minValueDecimal)
+                } else if let maxValue = maxVal, let maxValueDecimal: Decimal = settable?.get(maxValue) {
+                    constrainedValue = min(value, maxValueDecimal)
+                } else {
+                    constrainedValue = value
+                }
+                settable?.set(keypath, value: constrainedValue)
             case let (.insulinCurve(keypath), value as InsulinCurve):
                 settable?.set(keypath, value: value)
             default: break

Разница между файлами не показана из-за своего большого размера
+ 3 - 5
FreeAPS/Sources/Modules/PreferencesEditor/PreferencesEditorStateModel.swift


+ 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)

+ 2 - 0
FreeAPS/Sources/Modules/PumpConfig/View/PumpConfigRootView.swift

@@ -4,6 +4,7 @@ import Swinject
 extension PumpConfig {
     struct RootView: BaseView {
         let resolver: Resolver
+        let displayClose: Bool
         @StateObject var state = StateModel()
 
         var body: some View {
@@ -34,6 +35,7 @@ extension PumpConfig {
                 .onAppear(perform: configureView)
                 .navigationTitle("Pump config")
                 .navigationBarTitleDisplayMode(.automatic)
+                .navigationBarItems(leading: displayClose ? Button("Close", action: state.hideModal) : nil)
                 .sheet(isPresented: $state.setupPump) {
                     if let pumpManager = state.provider.apsManager.pumpManager {
                         PumpSettingsView(

+ 17 - 4
FreeAPS/Sources/Modules/PumpSettingsEditor/PumpSettingsEditorProvider.swift

@@ -19,7 +19,7 @@ extension PumpSettingsEditor {
         }
 
         func save(settings: PumpSettings) -> AnyPublisher<Void, Error> {
-            func save() {
+            func save(_ settings: PumpSettings) {
                 storage.save(settings, as: OpenAPS.Settings.settings)
                 processQueue.async {
                     self.broadcaster.notify(PumpSettingsObserver.self, on: self.processQueue) {
@@ -29,7 +29,7 @@ extension PumpSettingsEditor {
             }
 
             guard let pump = deviceManager?.pumpManager else {
-                save()
+                save(settings)
                 return Just(()).setFailureType(to: Error.self).eraseToAnyPublisher()
             }
             // Don't ask why 🤦‍♂️
@@ -44,8 +44,21 @@ extension PumpSettingsEditor {
                 self.processQueue.async {
                     pump.syncDeliveryLimits(limits: limits) { result in
                         switch result {
-                        case .success:
-                            save()
+                        case let .success(actual):
+                            // Store the limits from the pumpManager to ensure the correct values
+                            // Example: Dana pumps don't allow to set these limits, only to fetch them
+                            // This will ensure we always have the correct values stored
+                            save(PumpSettings(
+                                insulinActionCurve: settings.insulinActionCurve,
+                                maxBolus: Decimal(
+                                    actual.maximumBolus?
+                                        .doubleValue(for: .internationalUnit()) ?? Double(settings.maxBolus)
+                                ),
+                                maxBasal: Decimal(
+                                    actual.maximumBasalRate?
+                                        .doubleValue(for: .internationalUnitsPerHour) ?? Double(settings.maxBasal)
+                                )
+                            ))
                             promise(.success(()))
                         case let .failure(error):
                             promise(.failure(error))

+ 3 - 0
FreeAPS/Sources/Modules/PumpSettingsEditor/PumpSettingsEditorStateModel.swift

@@ -25,7 +25,10 @@ extension PumpSettingsEditor {
             provider.save(settings: settings)
                 .receive(on: DispatchQueue.main)
                 .sink { _ in
+                    let settings = self.provider.settings()
                     self.syncInProgress = false
+                    self.maxBasal = settings.maxBasal
+                    self.maxBolus = settings.maxBolus
 
                 } receiveValue: {}
                 .store(in: &lifetime)

+ 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!
     }
 }

+ 21 - 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 = ""
@@ -62,6 +62,23 @@ extension Settings {
         func hideSettingsModal() {
             hideModal()
         }
+
+        // Commenting this out for now, as not needed and possibly dangerous for users to be able to nuke their pump pairing informations via the debug menu
+        // Leaving it in here, as it may be a handy functionality for further testing or developers.
+        // See https://github.com/nightscout/Trio/pull/277 for more information
+//
+//        func resetLoopDocuments() {
+//            guard let localDocuments = try? FileManager.default.url(
+//                for: .documentDirectory,
+//                in: .userDomainMask,
+//                appropriateFor: nil,
+//                create: true
+//            ) else {
+//                preconditionFailure("Could not get a documents directory URL.")
+//            }
+//            let storageURL = localDocuments.appendingPathComponent("PumpManagerState" + ".plist")
+//            try? FileManager.default.removeItem(at: storageURL)
+//        }
     }
 }
 
@@ -75,7 +92,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 +103,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)
     }
 }

+ 13 - 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)
                     }
@@ -62,6 +61,16 @@ extension Settings {
                                     .frame(maxWidth: .infinity, alignment: .trailing)
                                     .buttonStyle(.borderedProminent)
                             }
+                            // Commenting this out for now, as not needed and possibly dangerous for users to be able to nuke their pump pairing informations via the debug menu
+                            // Leaving it in here, as it may be a handy functionality for further testing or developers.
+                            // See https://github.com/nightscout/Trio/pull/277 for more information
+//
+//                            HStack {
+//                                Text("Delete Stored Pump State Binary Files")
+//                                Button("Delete") { state.resetLoopDocuments() }
+//                                    .frame(maxWidth: .infinity, alignment: .trailing)
+//                                    .buttonStyle(.borderedProminent)
+//                            }
                         }
                         Group {
                             Text("Preferences")
@@ -130,26 +139,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
+                    )
+                }
+            }
+        }
+    }
+}

+ 2 - 2
FreeAPS/Sources/Modules/WatchConfig/View/WatchConfigRootView.swift

@@ -17,10 +17,10 @@ extension WatchConfig {
                             Text(v.displayName).tag(v)
                         }
                     }
+                    Toggle("Display Protein & Fat", isOn: $state.displayFatAndProteinOnWatch)
+                    Toggle("Confirm Bolus Faster", isOn: $state.confirmBolusFaster)
                 }
 
-                Toggle("Display Protein & Fat", isOn: $state.displayFatAndProteinOnWatch)
-
                 Section(header: Text("Garmin Watch")) {
                     List {
                         ForEach(state.devices, id: \.uuid) { device in

+ 2 - 0
FreeAPS/Sources/Modules/WatchConfig/WatchConfigStateModel.swift

@@ -31,6 +31,7 @@ extension WatchConfig {
         @Published var devices: [IQDevice] = []
         @Published var selectedAwConfig: AwConfig = .HR
         @Published var displayFatAndProteinOnWatch = false
+        @Published var confirmBolusFaster = false
 
         private(set) var preferences = Preferences()
 
@@ -38,6 +39,7 @@ extension WatchConfig {
             preferences = provider.preferences
 
             subscribeSetting(\.displayFatAndProteinOnWatch, on: $displayFatAndProteinOnWatch) { displayFatAndProteinOnWatch = $0 }
+            subscribeSetting(\.confirmBolusFaster, on: $confirmBolusFaster) { confirmBolusFaster = $0 }
             subscribeSetting(\.displayOnWatch, on: $selectedAwConfig) { selectedAwConfig = $0 }
             didSet: { [weak self] value in
                 // for compatibility with old displayHR

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

@@ -9,6 +9,7 @@ enum Screen: Identifiable, Hashable {
     case nighscoutConfig
     case nighscoutConfigDirect
     case pumpConfig
+    case pumpConfigDirect
     case pumpSettingsEditor
     case basalProfileEditor
     case isfEditor
@@ -53,7 +54,9 @@ extension Screen {
         case .nighscoutConfigDirect:
             NightscoutConfig.RootView(resolver: resolver, displayClose: true)
         case .pumpConfig:
-            PumpConfig.RootView(resolver: resolver)
+            PumpConfig.RootView(resolver: resolver, displayClose: false)
+        case .pumpConfigDirect:
+            PumpConfig.RootView(resolver: resolver, displayClose: true)
         case .pumpSettingsEditor:
             PumpSettingsEditor.RootView(resolver: resolver)
         case .basalProfileEditor:

+ 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
     }
 }
 

+ 1 - 0
FreeAPS/Sources/Services/WatchManager/WatchManager.swift

@@ -106,6 +106,7 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
             self.state.bolusAfterCarbs = !self.settingsManager.settings.skipBolusScreenAfterCarbs
             self.state.displayOnWatch = self.settingsManager.settings.displayOnWatch
             self.state.displayFatAndProteinOnWatch = self.settingsManager.settings.displayFatAndProteinOnWatch
+            self.state.confirmBolusFaster = self.settingsManager.settings.confirmBolusFaster
 
             let eBG = self.evetualBGStraing()
             self.state.eventualBG = eBG.map { "⇢ " + $0 }

+ 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 - 0
FreeAPSWatch WatchKit Extension/DataFlow.swift

@@ -22,6 +22,7 @@ struct WatchState: Codable {
     var eventualBGRaw: String?
     var displayOnWatch: AwConfig?
     var displayFatAndProteinOnWatch: Bool?
+    var confirmBolusFaster: Bool?
     var isf: Decimal?
     var override: String?
 }

+ 1 - 1
FreeAPSWatch WatchKit Extension/Views/BolusConfirmationView.swift

@@ -75,7 +75,7 @@ struct BolusConfirmationView: View {
             $crownProgress,
             from: 0.0,
             through: 100.0,
-            by: 0.5,
+            by: state.confirmBolusFaster ? 5 : 0.5,
             sensitivity: .high,
             isContinuous: false,
             isHapticFeedbackEnabled: true

+ 2 - 0
FreeAPSWatch WatchKit Extension/WatchStateModel.swift

@@ -34,6 +34,7 @@ class WatchStateModel: NSObject, ObservableObject {
     @Published var isBolusViewActive = false
     @Published var displayOnWatch: AwConfig = .BGTarget
     @Published var displayFatAndProteinOnWatch = false
+    @Published var confirmBolusFaster = false
     @Published var eventualBG = ""
     @Published var isConfirmationViewActive = false {
         didSet {
@@ -174,6 +175,7 @@ class WatchStateModel: NSObject, ObservableObject {
         eventualBG = state.eventualBG ?? ""
         displayOnWatch = state.displayOnWatch ?? .BGTarget
         displayFatAndProteinOnWatch = state.displayFatAndProteinOnWatch ?? false
+        confirmBolusFaster = state.confirmBolusFaster ?? false
         isf = state.isf
         override = state.override
     }

+ 1 - 1
G7SensorKit

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

+ 1 - 1
LiveActivity/LiveActivity.swift

@@ -288,7 +288,7 @@ struct LiveActivity: Widget {
                     label.fontWidth(.compressed)
                 }
             }
-            .widgetURL(URL(string: "freeaps-x://"))
+            .widgetURL(URL(string: "Trio://"))
             .keylineTint(Color.purple)
             .contentMargins(.horizontal, 0, for: .minimal)
             .contentMargins(.trailing, 0, for: .compactLeading)

+ 1 - 1
OmniBLE

@@ -1 +1 @@
-Subproject commit 91abd9aba338903cc7bccd3a4c9df5dc4452cc1f
+Subproject commit 85fc3c6d4805d580acdf6592b220717b6e842558

+ 1 - 1
OmniKit

@@ -1 +1 @@
-Subproject commit f51fe354ea6739ee09de922ed836d6844545b610
+Subproject commit a80e38b1b7f203014b461f8aff8cead2c067e39d

Разница между файлами не показана из-за своего большого размера
+ 22 - 9
README.md


+ 6 - 1
oref0_source_version.txt

@@ -1,6 +1,11 @@
-oref0 branch: dev - git version: e023125
+oref0 branch: dev - git version: d1dfb70
 
 Last commits:
+d1dfb70 Merge pull request #26 from MikePlante1/typo
+d9f1662 fix `threshold_setting` typo
+b454837 Merge pull request #24 from nightscout/Trio_renames
+5319e39 update Discord url
+5b7affa Github issue templates and config.yml: rename from Open-iAPS to Trio
 e023125 Replace Open-iAPS with Trio (#23)
 fa373c9 Merge pull request #22 from nightscout/tmhastings-tddAdjBasal
 fc0ae69 tddAdjBasal pop-up correction

+ 1 - 1
trio-oref/lib/profile/index.js

@@ -77,7 +77,7 @@ function defaults ( ) {
     , tddAdjBasal: false // Enable adjustment of basal based on the ratio of 24 h : 10 day average TDD
     , enableSMB_high_bg: false // enable SMBs when a high BG is detected, based on the high BG target (adjusted or profile)
     , enableSMB_high_bg_target: 110 // set the value enableSMB_high_bg will compare against to enable SMB. If BG > than this value, SMBs should enable.
-    , threshold_setting: 0.60 // Use a configurable threshold setting
+    , threshold_setting: 60 // Use a configurable threshold setting
   }
 }