Procházet zdrojové kódy

Merge branch 'nightscout:dev' into patch-1

Liroy van Hoewijk před 1 rokem
rodič
revize
7a093ff83f
93 změnil soubory, kde provedl 2249 přidání a 921 odebrání
  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"
 name: "\U0001F41B Bug report"
 about: Create a report to help us fix things
 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
 blank_issues_enabled: false
 contact_links:
 contact_links:
   - name: "🆘 Individual troubleshooting help: Please go to the Discord Trio Server"
   - 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
     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"
 name: "\U0001F4A1 Feature request \U0001F4A1"
 about: Suggest an idea for this project
 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
       # Upload Build artifacts
       - name: Upload build log, IPA and Symbol artifacts
       - name: Upload build log, IPA and Symbol artifacts
         if: always()
         if: always()
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
         with:
           name: build-artifacts
           name: build-artifacts
           path: |
           path: |

+ 20 - 4
FreeAPS.xcodeproj/project.pbxproj

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

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

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

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 1
FreeAPS/Resources/javascript/bundle/autosens.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 1
FreeAPS/Resources/javascript/bundle/autotune-prep.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 1
FreeAPS/Resources/javascript/bundle/iob.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 1
FreeAPS/Resources/javascript/bundle/meal.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 1
FreeAPS/Resources/javascript/bundle/profile.js


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

@@ -5,6 +5,7 @@
   "useAutotune" : false,
   "useAutotune" : false,
   "onlyAutotuneBasals" : false,
   "onlyAutotuneBasals" : false,
   "isUploadEnabled" : false,
   "isUploadEnabled" : false,
+  "isDownloadEnabled" : false,
   "useLocalGlucoseSource" : false,
   "useLocalGlucoseSource" : false,
   "localGlucosePort" : 8080,
   "localGlucosePort" : 8080,
   "debugOptions" : false,
   "debugOptions" : false,
@@ -40,6 +41,9 @@
   "oneDimensionalGraph" : false,
   "oneDimensionalGraph" : false,
   "rulerMarks" : true,
   "rulerMarks" : true,
   "maxCarbs": 250,
   "maxCarbs": 250,
+  "maxFat": 250,
+  "maxProtein": 250,
   "displayFatAndProteinOnWatch": false,
   "displayFatAndProteinOnWatch": false,
+  "confirmBolusFaster": false,
   "lockScreenView": "simple"
   "lockScreenView": "simple"
 }
 }

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

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

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

@@ -43,10 +43,6 @@ private let staticPumpManagersByIdentifier: [String: PumpManagerUI.Type] = [
     MockPumpManager.pluginIdentifier: MockPumpManager.self
     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")
 private let accessLock = NSRecursiveLock(label: "BaseDeviceDataManager.accessLock")
 
 
 final class BaseDeviceDataManager: DeviceDataManager, Injectable {
 final class BaseDeviceDataManager: DeviceDataManager, Injectable {
@@ -78,7 +74,8 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
         didSet {
         didSet {
             pumpManager?.pumpManagerDelegate = self
             pumpManager?.pumpManagerDelegate = self
             pumpManager?.delegateQueue = processQueue
             pumpManager?.delegateQueue = processQueue
-            UserDefaults.standard.pumpManagerRawValue = pumpManager?.rawValue
+            rawPumpManager = pumpManager?.rawValue
+            UserDefaults.standard.clearLegacyPumpManagerRawValue()
             if let pumpManager = pumpManager {
             if let pumpManager = pumpManager {
                 pumpDisplayState.value = PumpDisplayState(name: pumpManager.localizedTitle, image: pumpManager.smallImage)
                 pumpDisplayState.value = PumpDisplayState(name: pumpManager.localizedTitle, image: pumpManager.smallImage)
                 pumpName.send(pumpManager.localizedTitle)
                 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 bluetoothManager: BluetoothStateManager { bluetoothProvider }
 
 
     var hasBLEHeartbeat: Bool {
     var hasBLEHeartbeat: Bool {
@@ -123,7 +122,11 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
     }
     }
 
 
     func setupPumpManager() {
     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? {
     func createBolusProgressReporter() -> DoseProgressReporter? {
@@ -163,20 +166,6 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
                 self.updateUpdateFinished(true)
                 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) {
     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.")
             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()
         self.recommendsLoop.send()
     }
     }
 
 
@@ -319,7 +303,7 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
     }
     }
 
 
     func pumpManagerDidUpdateState(_ pumpManager: PumpManager) {
     func pumpManagerDidUpdateState(_ pumpManager: PumpManager) {
-        UserDefaults.standard.pumpManagerRawValue = pumpManager.rawValue
+        rawPumpManager = pumpManager.rawValue
         if self.pumpManager == nil, let newPumpManager = pumpManager as? PumpManagerUI {
         if self.pumpManager == nil, let newPumpManager = pumpManager as? PumpManagerUI {
             self.pumpManager = newPumpManager
             self.pumpManager = newPumpManager
         }
         }
@@ -431,6 +415,9 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
     func pumpManagerWillDeactivate(_: PumpManager) {
     func pumpManagerWillDeactivate(_: PumpManager) {
         dispatchPrecondition(condition: .onQueue(processQueue))
         dispatchPrecondition(condition: .onQueue(processQueue))
         pumpManager = nil
         pumpManager = nil
+        broadcaster.notify(PumpDeactivatedObserver.self, on: processQueue) {
+            $0.pumpDeactivatedDidChange()
+        }
     }
     }
 
 
     func pumpManager(_: PumpManager, didUpdatePumpRecordsBasalProfileStartEvents _: Bool) {}
     func pumpManager(_: PumpManager, didUpdatePumpRecordsBasalProfileStartEvents _: Bool) {}
@@ -537,29 +524,6 @@ extension BaseDeviceDataManager: DeviceManagerDelegate {
 
 
     func recordRetractedAlert(_: Alert, at _: Date) {}
     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]) {
     func removeNotificationRequests(for _: DeviceManager, identifiers: [String]) {
         DispatchQueue.main.async {
         DispatchQueue.main.async {
             UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: identifiers)
             UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: identifiers)
@@ -667,3 +631,7 @@ protocol PumpReservoirObserver {
 protocol PumpBatteryObserver {
 protocol PumpBatteryObserver {
     func pumpBatteryDidChange(_ battery: Battery)
     func pumpBatteryDidChange(_ battery: Battery)
 }
 }
+
+protocol PumpDeactivatedObserver {
+    func pumpDeactivatedDidChange()
+}

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

@@ -2,6 +2,8 @@ import LoopKit
 import LoopKitUI
 import LoopKitUI
 
 
 extension PumpManager {
 extension PumpManager {
+    typealias RawValue = [String: Any]
+
     var rawValue: [String: Any] {
     var rawValue: [String: Any] {
         [
         [
             "managerIdentifier": pluginIdentifier, // "managerIdentifier": type(of: self).managerIdentifier,
             "managerIdentifier": pluginIdentifier, // "managerIdentifier": type(of: self).managerIdentifier,
@@ -11,14 +13,6 @@ extension PumpManager {
 }
 }
 
 
 extension PumpManagerUI {
 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(
     func settingsViewController(
         bluetoothProvider: BluetoothProvider,
         bluetoothProvider: BluetoothProvider,
         pumpManagerOnboardingDelegate: PumpManagerOnboardingDelegate?
         pumpManagerOnboardingDelegate: PumpManagerOnboardingDelegate?
@@ -32,14 +26,6 @@ extension PumpManagerUI {
         vc.pumpManagerOnboardingDelegate = pumpManagerOnboardingDelegate
         vc.pumpManagerOnboardingDelegate = pumpManagerOnboardingDelegate
         return vc
         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 {
 protocol PumpSettingsBuilder {

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

@@ -5,17 +5,10 @@ import RileyLinkKit
 
 
 extension UserDefaults {
 extension UserDefaults {
     private enum Key: String {
     private enum Key: String {
-        case pumpManagerRawValue = "com.rileylink.PumpManagerRawValue"
+        case legacyPumpManagerRawValue = "com.rileylink.PumpManagerRawValue"
         case rileyLinkConnectionManagerState = "com.rileylink.RileyLinkConnectionManagerState"
         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? {
     var rileyLinkConnectionManagerState: RileyLinkConnectionState? {
@@ -30,4 +23,20 @@ extension UserDefaults {
             set(newValue?.rawValue, forKey: Key.rileyLinkConnectionManagerState.rawValue)
             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")
     private let processQueue = DispatchQueue(label: "BaseGlucoseManager.processQueue")
     @Injected() var glucoseStorage: GlucoseStorage!
     @Injected() var glucoseStorage: GlucoseStorage!
     @Injected() var nightscoutManager: NightscoutManager!
     @Injected() var nightscoutManager: NightscoutManager!
-    @Injected() var tidePoolService: TidePoolManager!
+    @Injected() var tidepoolService: TidepoolManager!
     @Injected() var apsManager: APSManager!
     @Injected() var apsManager: APSManager!
     @Injected() var settingsManager: SettingsManager!
     @Injected() var settingsManager: SettingsManager!
     @Injected() var healthKitManager: HealthKitManager!
     @Injected() var healthKitManager: HealthKitManager!
@@ -45,6 +45,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
     var cgmManager: CGMManagerUI? {
     var cgmManager: CGMManagerUI? {
         didSet {
         didSet {
             rawCGMManager = cgmManager?.rawValue
             rawCGMManager = cgmManager?.rawValue
+            UserDefaults.standard.clearLegacyCGMManagerRawValue()
         }
         }
     }
     }
 
 
@@ -235,7 +236,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         deviceDataManager.heartbeat(date: Date())
         deviceDataManager.heartbeat(date: Date())
 
 
         nightscoutManager.uploadGlucose()
         nightscoutManager.uploadGlucose()
-        tidePoolService.uploadGlucose(device: cgmManager?.cgmManagerStatus.device)
+        tidepoolService.uploadGlucose(device: cgmManager?.cgmManagerStatus.device)
 
 
         let glucoseForHealth = filteredByDate.filter { !glucoseFromHealth.contains($0) }
         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)
         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]) {
     func storeCarbs(_ entries: [CarbsEntry]) {
         processQueue.sync {
         processQueue.sync {
             let file = OpenAPS.Monitor.carbHistory
             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 {
                 if carbEquivalents > 0 {
                     self.storage.transaction { storage in
                     self.storage.transaction { storage in
                         storage.append(futureCarbArray, to: file, uniqBy: \.id)
                         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() }
                             .filter { $0.createdAt.addingTimeInterval(1.days.timeInterval) > Date() }
                             .sorted { $0.createdAt > $1.createdAt } ?? []
                             .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
                 self.storage.transaction { storage in
                     storage.append(entries, to: file, uniqBy: \.createdAt)
                     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() }
                         .filter { $0.createdAt.addingTimeInterval(1.days.timeInterval) > Date() }
                         .sorted { $0.createdAt > $1.createdAt } ?? []
                         .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 cbs: Decimal = 0
             var carbDate = Date()
             var carbDate = Date()
             if entries.isNotEmpty {
             if entries.isNotEmpty {
@@ -131,11 +91,96 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
                 }
                 }
             }
             }
             broadcaster.notify(CarbsObserver.self, on: processQueue) {
             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 {
     func syncDate() -> Date {
         Date().addingTimeInterval(-1.days.timeInterval)
         Date().addingTimeInterval(-1.days.timeInterval)
     }
     }
@@ -187,8 +232,8 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
                 bolus: nil,
                 bolus: nil,
                 insulin: nil,
                 insulin: nil,
                 carbs: $0.carbs,
                 carbs: $0.carbs,
-                fat: nil,
-                protein: nil,
+                fat: $0.fat,
+                protein: $0.protein,
                 foodType: $0.note,
                 foodType: $0.note,
                 targetTop: nil,
                 targetTop: nil,
                 targetBottom: nil
                 targetBottom: nil

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

@@ -332,8 +332,8 @@ extension NightscoutTreatment {
                 insulin: nil,
                 insulin: nil,
                 notes: nil,
                 notes: nil,
                 carbs: Decimal(event.carbInput ?? 0),
                 carbs: Decimal(event.carbInput ?? 0),
-                fat: nil,
-                protein: nil,
+                fat: Decimal(event.fatInput ?? 0),
+                protein: Decimal(event.proteinInput ?? 0),
                 targetTop: nil,
                 targetTop: nil,
                 targetBottom: 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(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
         self.key = key
 
 
         let documents: URL
         let documents: URL
-
         guard let localDocuments = try? FileManager.default.url(
         guard let localDocuments = try? FileManager.default.url(
             for: .documentDirectory,
             for: .documentDirectory,
             in: .userDomainMask,
             in: .userDomainMask,

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 29 - 8
FreeAPS/Sources/Localizations/Main/ar.lproj/Localizable.strings


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 32 - 5
FreeAPS/Sources/Localizations/Main/ca.lproj/Localizable.strings


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 29 - 8
FreeAPS/Sources/Localizations/Main/da.lproj/Localizable.strings


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 29 - 8
FreeAPS/Sources/Localizations/Main/de.lproj/Localizable.strings


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 29 - 8
FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 29 - 8
FreeAPS/Sources/Localizations/Main/es.lproj/Localizable.strings


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 29 - 8
FreeAPS/Sources/Localizations/Main/fi.lproj/Localizable.strings


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 29 - 8
FreeAPS/Sources/Localizations/Main/fr.lproj/Localizable.strings


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 29 - 8
FreeAPS/Sources/Localizations/Main/he.lproj/Localizable.strings


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 29 - 8
FreeAPS/Sources/Localizations/Main/hu.lproj/Localizable.strings


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 29 - 8
FreeAPS/Sources/Localizations/Main/it.lproj/Localizable.strings


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 29 - 8
FreeAPS/Sources/Localizations/Main/nb.lproj/Localizable.strings


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 29 - 8
FreeAPS/Sources/Localizations/Main/nl.lproj/Localizable.strings


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 29 - 8
FreeAPS/Sources/Localizations/Main/pl.lproj/Localizable.strings


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 29 - 8
FreeAPS/Sources/Localizations/Main/pt-BR.lproj/Localizable.strings


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 29 - 8
FreeAPS/Sources/Localizations/Main/pt-PT.lproj/Localizable.strings


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 29 - 8
FreeAPS/Sources/Localizations/Main/ru.lproj/Localizable.strings


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 29 - 8
FreeAPS/Sources/Localizations/Main/sk.lproj/Localizable.strings


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 29 - 8
FreeAPS/Sources/Localizations/Main/sv.lproj/Localizable.strings


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 29 - 8
FreeAPS/Sources/Localizations/Main/tr.lproj/Localizable.strings


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 29 - 8
FreeAPS/Sources/Localizations/Main/uk.lproj/Localizable.strings


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 29 - 8
FreeAPS/Sources/Localizations/Main/vi.lproj/Localizable.strings


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 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 allowAnnouncements: Bool = false
     var useAutotune: Bool = false
     var useAutotune: Bool = false
     var isUploadEnabled: Bool = false
     var isUploadEnabled: Bool = false
+    var isDownloadEnabled: Bool = false
     var useLocalGlucoseSource: Bool = false
     var useLocalGlucoseSource: Bool = false
     var localGlucosePort: Int = 8080
     var localGlucosePort: Int = 8080
     var debugOptions: Bool = false
     var debugOptions: Bool = false
@@ -41,7 +42,10 @@ struct FreeAPSSettings: JSON, Equatable {
     var oneDimensionalGraph: Bool = false
     var oneDimensionalGraph: Bool = false
     var rulerMarks: Bool = true
     var rulerMarks: Bool = true
     var maxCarbs: Decimal = 250
     var maxCarbs: Decimal = 250
+    var maxFat: Decimal = 250
+    var maxProtein: Decimal = 250
     var displayFatAndProteinOnWatch: Bool = false
     var displayFatAndProteinOnWatch: Bool = false
+    var confirmBolusFaster: Bool = false
     var onlyAutotuneBasals: Bool = false
     var onlyAutotuneBasals: Bool = false
     var useLiveActivity: Bool = false
     var useLiveActivity: Bool = false
     var lockScreenView: LockScreenView = .simple
     var lockScreenView: LockScreenView = .simple
@@ -73,6 +77,10 @@ extension FreeAPSSettings: Decodable {
             settings.isUploadEnabled = isUploadEnabled
             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) {
         if let useLocalGlucoseSource = try? container.decode(Bool.self, forKey: .useLocalGlucoseSource) {
             settings.useLocalGlucoseSource = useLocalGlucoseSource
             settings.useLocalGlucoseSource = useLocalGlucoseSource
         }
         }
@@ -218,10 +226,22 @@ extension FreeAPSSettings: Decodable {
             settings.maxCarbs = maxCarbs
             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) {
         if let displayFatAndProteinOnWatch = try? container.decode(Bool.self, forKey: .displayFatAndProteinOnWatch) {
             settings.displayFatAndProteinOnWatch = 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) {
         if let onlyAutotuneBasals = try? container.decode(Bool.self, forKey: .onlyAutotuneBasals) {
             settings.onlyAutotuneBasals = onlyAutotuneBasals
             settings.onlyAutotuneBasals = onlyAutotuneBasals
         }
         }

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

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

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

@@ -4,16 +4,12 @@ struct FetchedNightscoutProfileStore: JSON {
     let _id: String
     let _id: String
     let defaultProfile: String
     let defaultProfile: String
     let startDate: String
     let startDate: String
-    let mills: Decimal
     let enteredBy: String
     let enteredBy: String
-    let store: [String: ScheduledNightscoutProfile]
-    let created_at: String
+    let store: [String: FetchedNightscoutProfile]
 }
 }
 
 
 struct FetchedNightscoutProfile: JSON {
 struct FetchedNightscoutProfile: JSON {
     let dia: Decimal
     let dia: Decimal
-    let carbs_hr: Int
-    let delay: Decimal
     let timezone: String
     let timezone: String
     let target_low: [NightscoutTimevalue]
     let target_low: [NightscoutTimevalue]
     let target_high: [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 dish: String = ""
         @Published var selection: Presets?
         @Published var selection: Presets?
         @Published var summation: [String] = []
         @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 = ""
         @Published var note: String = ""
 
 
         let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
         let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
@@ -25,6 +27,8 @@ extension AddCarbs {
             subscribeSetting(\.useFPUconversion, on: $useFPUconversion) { useFPUconversion = $0 }
             subscribeSetting(\.useFPUconversion, on: $useFPUconversion) { useFPUconversion = $0 }
             carbsRequired = provider.suggestion?.carbsReq
             carbsRequired = provider.suggestion?.carbsReq
             maxCarbs = settings.settings.maxCarbs
             maxCarbs = settings.settings.maxCarbs
+            maxFat = settings.settings.maxFat
+            maxProtein = settings.settings.maxProtein
         }
         }
 
 
         func add() {
         func add() {
@@ -160,5 +164,17 @@ extension AddCarbs {
             }
             }
             return waitersNotepadString
             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()
         @StateObject var state = StateModel()
         @State var dish: String = ""
         @State var dish: String = ""
         @State var isPromtPresented = false
         @State var isPromtPresented = false
-        @State var saved = false
+        @State var noteSaved = false
+        @State var mealSaved = false
         @State private var showAlert = false
         @State private var showAlert = false
         @FocusState private var isFocused: Bool
         @FocusState private var isFocused: Bool
 
 
@@ -48,7 +49,7 @@ extension AddCarbs {
                             autofocus: true,
                             autofocus: true,
                             cleanInput: true
                             cleanInput: true
                         )
                         )
-                        Text("grams").foregroundColor(.secondary)
+                        Text(state.carbs > state.maxCarbs ? "⚠️" : "g").foregroundColor(.secondary)
                     }.padding(.vertical)
                     }.padding(.vertical)
 
 
                     if state.useFPUconversion {
                     if state.useFPUconversion {
@@ -57,7 +58,7 @@ extension AddCarbs {
                     HStack {
                     HStack {
                         Text("Note").foregroundColor(.secondary)
                         Text("Note").foregroundColor(.secondary)
                         TextField("", text: $state.note).multilineTextAlignment(.trailing)
                         TextField("", text: $state.note).multilineTextAlignment(.trailing)
-                        if state.note != "", isFocused {
+                        if isFocused {
                             Button { isFocused = false } label: { Image(systemName: "keyboard.chevron.compact.down") }
                             Button { isFocused = false } label: { Image(systemName: "keyboard.chevron.compact.down") }
                                 .controlSize(.mini)
                                 .controlSize(.mini)
                         }
                         }
@@ -117,9 +118,23 @@ extension AddCarbs {
                 }
                 }
 
 
                 Section {
                 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)
                         .frame(maxWidth: .infinity, alignment: .center)
                 } footer: { Text(state.waitersNotepad().description) }
                 } footer: { Text(state.waitersNotepad().description) }
 
 
@@ -138,8 +153,8 @@ extension AddCarbs {
                 Section {
                 Section {
                     TextField("Name Of Dish", text: $dish)
                     TextField("Name Of Dish", text: $dish)
                     Button {
                     Button {
-                        saved = true
-                        if dish != "", saved {
+                        noteSaved = true
+                        if dish != "", noteSaved {
                             let preset = Presets(context: moc)
                             let preset = Presets(context: moc)
                             preset.dish = dish
                             preset.dish = dish
                             preset.fat = state.fat as NSDecimalNumber
                             preset.fat = state.fat as NSDecimalNumber
@@ -147,14 +162,14 @@ extension AddCarbs {
                             preset.carbs = state.carbs as NSDecimalNumber
                             preset.carbs = state.carbs as NSDecimalNumber
                             try? moc.save()
                             try? moc.save()
                             state.addNewPresetToWaitersNotepad(dish)
                             state.addNewPresetToWaitersNotepad(dish)
-                            saved = false
+                            noteSaved = false
                             isPromtPresented = false
                             isPromtPresented = false
                         }
                         }
                     }
                     }
                     label: { Text("Save") }
                     label: { Text("Save") }
                     Button {
                     Button {
                         dish = ""
                         dish = ""
-                        saved = false
+                        noteSaved = false
                         isPromtPresented = false }
                         isPromtPresented = false }
                     label: { Text("Cancel") }
                     label: { Text("Cancel") }
                 } header: { Text("Enter Meal Preset Name") }
                 } header: { Text("Enter Meal Preset Name") }
@@ -260,7 +275,7 @@ extension AddCarbs {
                     autofocus: false,
                     autofocus: false,
                     cleanInput: true
                     cleanInput: true
                 )
                 )
-                Text("grams").foregroundColor(.secondary)
+                Text(state.fat > state.maxFat ? "⚠️" : "g").foregroundColor(.secondary)
             }
             }
             HStack {
             HStack {
                 Text("Protein").foregroundColor(.red) // .fontWeight(.thin)
                 Text("Protein").foregroundColor(.red) // .fontWeight(.thin)
@@ -271,9 +286,8 @@ extension AddCarbs {
                     formatter: formatter,
                     formatter: formatter,
                     autofocus: false,
                     autofocus: false,
                     cleanInput: true
                     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
         let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
 
 
         @Published var low: Decimal = 0
         @Published var low: Decimal = 0
-        // @Published var target: Decimal = 0
         @Published var high: Decimal = 0
         @Published var high: Decimal = 0
         @Published var duration: Decimal = 0
         @Published var duration: Decimal = 0
         @Published var date = Date()
         @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() {
         func save() {
             guard duration > 0 else {
             guard duration > 0 else {
                 return
                 return
@@ -158,7 +165,6 @@ extension AddTempTarget {
                         saveToCoreData.active = true
                         saveToCoreData.active = true
                         saveToCoreData.date = Date()
                         saveToCoreData.date = Date()
                         saveToCoreData.hbt = whichID?.hbt ?? 160
                         saveToCoreData.hbt = whichID?.hbt ?? 160
-                        // saveToCoreData.id = id
                         saveToCoreData.startDate = Date()
                         saveToCoreData.startDate = Date()
                         saveToCoreData.duration = whichID?.duration ?? 0
                         saveToCoreData.duration = whichID?.duration ?? 0
 
 
@@ -189,5 +195,46 @@ extension AddTempTarget {
             }
             }
             return Decimal(Double(target))
             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 isRemoveAlertPresented = false
         @State private var removeAlert: Alert?
         @State private var removeAlert: Alert?
         @State private var isEditing = false
         @State private var isEditing = false
+        @State private var selectedPreset: TempTarget?
+        @State private var isEditSheetPresented = false
 
 
         @FetchRequest(
         @FetchRequest(
             entity: TempTargetsSlider.entity(),
             entity: TempTargetsSlider.entity(),
@@ -23,104 +25,73 @@ extension AddTempTarget {
             return formatter
             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 {
         var body: some View {
             Form {
             Form {
                 if !state.presets.isEmpty {
                 if !state.presets.isEmpty {
                     Section(header: Text("Presets")) {
                     Section(header: Text("Presets")) {
                         ForEach(state.presets) { preset in
                         ForEach(state.presets) { preset in
                             presetView(for: preset)
                             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 {
                 Section {
                     Button { state.enact() }
                     Button { state.enact() }
                     label: { Text("Enact") }
                     label: { Text("Enact") }
+                        .disabled(state.duration == 0)
                     Button { state.cancel() }
                     Button { state.cancel() }
                     label: { Text("Cancel Temp Target") }
                     label: { Text("Cancel Temp Target") }
                 }
                 }
@@ -129,6 +100,8 @@ extension AddTempTarget {
                 Form {
                 Form {
                     Section(header: Text("Enter preset name")) {
                     Section(header: Text("Enter preset name")) {
                         TextField("Name", text: $state.newPresetName)
                         TextField("Name", text: $state.newPresetName)
+                    }
+                    Section {
                         Button {
                         Button {
                             state.save()
                             state.save()
                             isPromtPresented = false
                             isPromtPresented = false
@@ -139,6 +112,10 @@ extension AddTempTarget {
                     }
                     }
                 }
                 }
             }
             }
+            .sheet(isPresented: $isEditSheetPresented) {
+                editPresetPopover()
+                    .padding()
+            }
             .onAppear {
             .onAppear {
                 configureView()
                 configureView()
                 state.hbt = isEnabledArray.first?.hbt ?? 160
                 state.hbt = isEnabledArray.first?.hbt ?? 160
@@ -148,13 +125,141 @@ extension AddTempTarget {
             .navigationBarItems(leading: Button("Close", action: state.hideModal))
             .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 {
         private func presetView(for preset: TempTarget) -> some View {
             var low = preset.targetBottom
             var low = preset.targetBottom
-            var high = preset.targetTop
             if state.units == .mmolL {
             if state.units == .mmolL {
                 low = low?.asMmolL
                 low = low?.asMmolL
-                high = high?.asMmolL
             }
             }
+
             return HStack {
             return HStack {
                 VStack {
                 VStack {
                     HStack {
                     HStack {
@@ -162,11 +267,13 @@ extension AddTempTarget {
                         Spacer()
                         Spacer()
                     }
                     }
                     HStack(spacing: 2) {
                     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)
                         Text(state.units.rawValue)
                             .foregroundColor(.secondary)
                             .foregroundColor(.secondary)
@@ -182,29 +289,12 @@ extension AddTempTarget {
                             .font(.caption)
                             .font(.caption)
 
 
                         Spacer()
                         Spacer()
-                    }.padding(.top, 2)
+                    }.padding(.bottom, 2)
                 }
                 }
                 .contentShape(Rectangle())
                 .contentShape(Rectangle())
                 .onTapGesture {
                 .onTapGesture {
                     state.enactPreset(id: preset.id)
                     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,
                                 autofocus: true,
                                 cleanInput: true
                                 cleanInput: true
                             )
                             )
-                            Text("U").foregroundColor(.secondary)
+                            Text(state.amount > state.maxBolus ? "⚠️" : "U").foregroundColor(.secondary)
                         }
                         }
                     }
                     }
                     header: { Text("Bolus") }
                     header: { Text("Bolus") }
@@ -81,15 +81,19 @@ extension Bolus {
                         label: {
                         label: {
                             Text(
                             Text(
                                 state.amount <= state.maxBolus ? NSLocalizedString("Enact bolus", comment: "") :
                                 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)!
                                     + formatter.string(from: state.maxBolus as NSNumber)!
                                     + NSLocalizedString("U", comment: "Insulin unit")
                                     + 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 {
                     if waitForSuggestion {
                         Section {
                         Section {

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

@@ -8,7 +8,7 @@ extension DataTable {
         @Injected() var carbsStorage: CarbsStorage!
         @Injected() var carbsStorage: CarbsStorage!
         @Injected() var nightscoutManager: NightscoutManager!
         @Injected() var nightscoutManager: NightscoutManager!
         @Injected() var healthkitManager: HealthKitManager!
         @Injected() var healthkitManager: HealthKitManager!
-        @Injected() var tidePoolManager: TidePoolManager!
+        @Injected() var tidepoolManager: TidepoolManager!
 
 
         func pumpHistory() -> [PumpHistoryEvent] {
         func pumpHistory() -> [PumpHistoryEvent] {
             pumpHistoryStorage.recent()
             pumpHistoryStorage.recent()
@@ -33,10 +33,10 @@ extension DataTable {
         }
         }
 
 
         func deleteCarbs(_ treatement: Treatment) {
         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
             // probably to revise the logic
             // TODO:
             // TODO:
-            tidePoolManager.deleteCarbs(
+            tidepoolManager.deleteCarbs(
                 at: treatement.date,
                 at: treatement.date,
                 isFPU: treatement.isFPU,
                 isFPU: treatement.isFPU,
                 fpuID: treatement.fpuID,
                 fpuID: treatement.fpuID,
@@ -52,8 +52,8 @@ extension DataTable {
         }
         }
 
 
         func deleteInsulin(_ treatement: Treatment) {
         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)
             nightscoutManager.deleteInsulin(at: treatement.date)
             if let id = treatement.idPumpEvent {
             if let id = treatement.idPumpEvent {
                 healthkitManager.deleteInsulin(syncID: id)
                 healthkitManager.deleteInsulin(syncID: id)

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

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

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

@@ -28,11 +28,19 @@ extension FPUConfig {
 
 
         var body: some View {
         var body: some View {
             Form {
             Form {
-                Section(header: Text("Carbohydrate limit")) {
+                Section(header: Text("Limit Per Entry")) {
                     HStack {
                     HStack {
                         Text("Max Carbs")
                         Text("Max Carbs")
                         DecimalTextField("g", value: $state.maxCarbs, formatter: formatter)
                         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")) {
                 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 displayYgridLines: Bool = false
         @Published var thresholdLines: Bool = false
         @Published var thresholdLines: Bool = false
         @Published var cgmAvailable: Bool = false
         @Published var cgmAvailable: Bool = false
+        @Published var pumpStatusHighlightMessage: String? = nil
 
 
         let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
         let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
 
 
@@ -106,6 +107,7 @@ extension Home {
             broadcaster.register(EnactedSuggestionObserver.self, observer: self)
             broadcaster.register(EnactedSuggestionObserver.self, observer: self)
             broadcaster.register(PumpBatteryObserver.self, observer: self)
             broadcaster.register(PumpBatteryObserver.self, observer: self)
             broadcaster.register(PumpReservoirObserver.self, observer: self)
             broadcaster.register(PumpReservoirObserver.self, observer: self)
+            broadcaster.register(PumpDeactivatedObserver.self, observer: self)
 
 
             animatedBackground = settingsManager.settings.animatedBackground
             animatedBackground = settingsManager.settings.animatedBackground
 
 
@@ -168,6 +170,7 @@ extension Home {
                     } else {
                     } else {
                         self.setupBattery()
                         self.setupBattery()
                         self.setupReservoir()
                         self.setupReservoir()
+                        self.displayPumpStatusHighlightMessage()
                     }
                     }
                 }
                 }
                 .store(in: &lifetime)
                 .store(in: &lifetime)
@@ -185,6 +188,8 @@ extension Home {
                             setupDelegate: self
                             setupDelegate: self
                         ).asAny()
                         ).asAny()
                         self.router.mainSecondaryModalView.send(view)
                         self.router.mainSecondaryModalView.send(view)
+                    } else if show {
+                        self.router.mainSecondaryModalView.send(self.router.view(for: .pumpConfigDirect))
                     } else {
                     } else {
                         self.router.mainSecondaryModalView.send(nil)
                         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() {
         private func setupCurrentTempTarget() {
             tempTarget = provider.tempTarget()
             tempTarget = provider.tempTarget()
         }
         }
@@ -376,7 +397,8 @@ extension Home.StateModel:
     CarbsObserver,
     CarbsObserver,
     EnactedSuggestionObserver,
     EnactedSuggestionObserver,
     PumpBatteryObserver,
     PumpBatteryObserver,
-    PumpReservoirObserver
+    PumpReservoirObserver,
+    PumpDeactivatedObserver
 {
 {
     func glucoseDidUpdate(_: [BloodGlucose]) {
     func glucoseDidUpdate(_: [BloodGlucose]) {
         setupGlucose()
         setupGlucose()
@@ -410,6 +432,7 @@ extension Home.StateModel:
         setupBasals()
         setupBasals()
         setupBoluses()
         setupBoluses()
         setupSuspensions()
         setupSuspensions()
+        displayPumpStatusHighlightMessage()
     }
     }
 
 
     func pumpSettingsDidChange(_: PumpSettings) {
     func pumpSettingsDidChange(_: PumpSettings) {
@@ -435,10 +458,16 @@ extension Home.StateModel:
 
 
     func pumpBatteryDidChange(_: Battery) {
     func pumpBatteryDidChange(_: Battery) {
         setupBattery()
         setupBattery()
+        displayPumpStatusHighlightMessage()
     }
     }
 
 
     func pumpReservoirDidChange(_: Decimal) {
     func pumpReservoirDidChange(_: Decimal) {
         setupReservoir()
         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 name: String
     @Binding var expiresAtDate: Date?
     @Binding var expiresAtDate: Date?
     @Binding var timerDate: Date
     @Binding var timerDate: Date
+    @Binding var pumpStatusHighlightMessage: String?
 
 
     private var reservoirFormatter: NumberFormatter {
     private var reservoirFormatter: NumberFormatter {
         let formatter = NumberFormatter()
         let formatter = NumberFormatter()
@@ -21,48 +22,67 @@ struct PumpView: View {
     }
     }
 
 
     var body: some 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)
                             .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,
                 battery: $state.battery,
                 name: $state.pumpName,
                 name: $state.pumpName,
                 expiresAtDate: $state.pumpExpiresAtDate,
                 expiresAtDate: $state.pumpExpiresAtDate,
-                timerDate: $state.timerDate
+                timerDate: $state.timerDate,
+                pumpStatusHighlightMessage: $state.pumpStatusHighlightMessage
             )
             )
             .onTapGesture {
             .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 connecting = false
         @Published var backfilling = false
         @Published var backfilling = false
         @Published var isUploadEnabled = false // Allow uploads
         @Published var isUploadEnabled = false // Allow uploads
+        @Published var isDownloadEnabled = false // Allow downloads
         @Published var uploadGlucose = true // Upload Glucose
         @Published var uploadGlucose = true // Upload Glucose
         @Published var changeUploadGlucose = true // if plugin, need to be change in CGM configuration
         @Published var changeUploadGlucose = true // if plugin, need to be change in CGM configuration
         @Published var useLocalSource = false
         @Published var useLocalSource = false
@@ -43,6 +44,7 @@ extension NightscoutConfig {
 
 
             subscribeSetting(\.allowAnnouncements, on: $allowAnnouncements) { allowAnnouncements = $0 }
             subscribeSetting(\.allowAnnouncements, on: $allowAnnouncements) { allowAnnouncements = $0 }
             subscribeSetting(\.isUploadEnabled, on: $isUploadEnabled) { isUploadEnabled = $0 }
             subscribeSetting(\.isUploadEnabled, on: $isUploadEnabled) { isUploadEnabled = $0 }
+            subscribeSetting(\.isDownloadEnabled, on: $isDownloadEnabled) { isDownloadEnabled = $0 }
             subscribeSetting(\.useLocalGlucoseSource, on: $useLocalSource) { useLocalSource = $0 }
             subscribeSetting(\.useLocalGlucoseSource, on: $useLocalSource) { useLocalSource = $0 }
             subscribeSetting(\.localGlucosePort, on: $localPort.map(Int.init)) { localPort = Decimal($0) }
             subscribeSetting(\.localGlucosePort, on: $localPort.map(Int.init)) { localPort = Decimal($0) }
             subscribeSetting(\.uploadGlucose, on: $uploadGlucose, initial: { uploadGlucose = $0 })
             subscribeSetting(\.uploadGlucose, on: $uploadGlucose, initial: { uploadGlucose = $0 })
@@ -86,6 +88,24 @@ extension NightscoutConfig {
             return NightscoutAPI(url: url, secret: secret)
             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() {
         func importSettings() {
             guard let nightscout = nightscoutAPI else {
             guard let nightscout = nightscoutAPI else {
                 saveError("Can't access nightscoutAPI")
                 saveError("Can't access nightscoutAPI")
@@ -135,7 +155,11 @@ extension NightscoutConfig {
                 {
                 {
                     do {
                     do {
                         let fetchedProfileStore = try jsonDecoder.decode([FetchedNightscoutProfileStore].self, from: data)
                         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 {
                         else {
                             error = "\nCan't find the default Nightscout Profile."
                             error = "\nCan't find the default Nightscout Profile."
                             group.leave()
                             group.leave()
@@ -220,9 +244,15 @@ extension NightscoutConfig {
 
 
                         let targets = fetchedProfile.target_low
                         let targets = fetchedProfile.target_low
                             .map { target -> BGTargetEntry in
                             .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,
                                     start: target.time,
                                     offset: self.offset(target.time) / 60
                                     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 resolver: Resolver
         let displayClose: Bool
         let displayClose: Bool
         @StateObject var state = StateModel()
         @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(
         @FetchRequest(
             entity: ImportError.entity(),
             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>
         ) var fetchedErrors: FetchedResults<ImportError>
 
 
-        private var portFormater: NumberFormatter {
-            let formatter = NumberFormatter()
-            formatter.allowsFloats = false
-            return formatter
-        }
-
         var body: some View {
         var body: some View {
             Form {
             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(
                         importAlert = Alert(
                             title: Text("Import settings?"),
                             title: Text("Import settings?"),
                             message: Text(
                             message: Text(
@@ -94,50 +58,38 @@ extension NightscoutConfig {
                         )
                         )
                         isImportAlertPresented.toggle()
                         isImportAlertPresented.toggle()
                     }.disabled(state.url.isEmpty || state.connecting)
                     }.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 {
                 Section {
                     Button("Backfill glucose") { state.backfillGlucose() }
                     Button("Backfill glucose") { state.backfillGlucose() }
                         .disabled(state.url.isEmpty || state.connecting || state.backfilling)
                         .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")
             .navigationBarTitle("Nightscout Config")
             .navigationBarTitleDisplayMode(.automatic)
             .navigationBarTitleDisplayMode(.automatic)
             .navigationBarItems(leading: displayClose ? Button("Close", action: state.hideModal) : nil)
             .navigationBarItems(leading: displayClose ? Button("Close", action: state.hideModal) : nil)
             .alert(isPresented: $isImportAlertPresented) {
             .alert(isPresented: $isImportAlertPresented) {
                 importAlert!
                 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
         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() {
         override func subscribe() {
             units = settingsManager.settings.units
             units = settingsManager.settings.units
             defaultSmbMinutes = settingsManager.preferences.maxSMBBasalMinutes
             defaultSmbMinutes = settingsManager.preferences.maxSMBBasalMinutes
@@ -38,6 +56,59 @@ extension OverrideProfilesConfig {
 
 
         let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
         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() {
         func saveSettings() {
             coredataContext.perform { [self] in
             coredataContext.perform { [self] in
                 let saveOverride = Override(context: self.coredataContext)
                 let saveOverride = Override(context: self.coredataContext)
@@ -87,6 +158,7 @@ extension OverrideProfilesConfig {
                 saveOverride.percentage = self.percentage
                 saveOverride.percentage = self.percentage
                 saveOverride.smbIsOff = self.smbIsOff
                 saveOverride.smbIsOff = self.smbIsOff
                 saveOverride.name = self.profileName
                 saveOverride.name = self.profileName
+                self.profileName = ""
                 id = UUID().uuidString
                 id = UUID().uuidString
                 self.isPreset.toggle()
                 self.isPreset.toggle()
                 saveOverride.id = id
                 saveOverride.id = id
@@ -166,7 +238,6 @@ extension OverrideProfilesConfig {
                 let requestEnabled = Override.fetchRequest() as NSFetchRequest<Override>
                 let requestEnabled = Override.fetchRequest() as NSFetchRequest<Override>
                 let sortIsEnabled = NSSortDescriptor(key: "date", ascending: false)
                 let sortIsEnabled = NSSortDescriptor(key: "date", ascending: false)
                 requestEnabled.sortDescriptors = [sortIsEnabled]
                 requestEnabled.sortDescriptors = [sortIsEnabled]
-                // requestEnabled.fetchLimit = 1
                 try? overrideArray = coredataContext.fetch(requestEnabled)
                 try? overrideArray = coredataContext.fetch(requestEnabled)
                 isEnabled = overrideArray.first?.enabled ?? false
                 isEnabled = overrideArray.first?.enabled ?? false
                 percentage = overrideArray.first?.percentage ?? 100
                 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() {
         func cancelProfile() {
             _indefinite = true
             _indefinite = true
             isEnabled = false
             isEnabled = false
@@ -247,5 +341,26 @@ extension OverrideProfilesConfig {
             smbMinutes = defaultSmbMinutes
             smbMinutes = defaultSmbMinutes
             uamMinutes = defaultUamMinutes
             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 CoreData
+import Foundation
 import SwiftUI
 import SwiftUI
 import Swinject
 import Swinject
 
 
@@ -10,8 +11,14 @@ extension OverrideProfilesConfig {
         @State private var isEditing = false
         @State private var isEditing = false
         @State private var showAlert = false
         @State private var showAlert = false
         @State private var showingDetail = false
         @State private var showingDetail = false
+        @State private var selectedPreset: OverridePresets?
+        @State private var isEditSheetPresented: Bool = false
         @State private var alertSring = ""
         @State private var alertSring = ""
         @State var isSheetPresented: Bool = false
         @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(\.dismiss) var dismiss
         @Environment(\.managedObjectContext) var moc
         @Environment(\.managedObjectContext) var moc
@@ -22,6 +29,7 @@ extension OverrideProfilesConfig {
                 format: "name != %@", "" as String
                 format: "name != %@", "" as String
             )
             )
         ) var fetchedProfiles: FetchedResults<OverridePresets>
         ) var fetchedProfiles: FetchedResults<OverridePresets>
+        var units: GlucoseUnits = .mmolL
 
 
         private var formatter: NumberFormatter {
         private var formatter: NumberFormatter {
             let formatter = NumberFormatter()
             let formatter = NumberFormatter()
@@ -43,144 +51,246 @@ extension OverrideProfilesConfig {
 
 
         var presetPopover: some View {
         var presetPopover: some View {
             Form {
             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 {
                 Section {
                     Button("Save") {
                     Button("Save") {
                         state.savePreset()
                         state.savePreset()
                         isSheetPresented = false
                         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") {
                     Button("Cancel") {
                         isSheetPresented = false
                         isSheetPresented = false
                     }
                     }
+                    .tint(.red)
                 }
                 }
             }
             }
         }
         }
 
 
-        var body: some View {
+        var editPresetPopover: some View {
             Form {
             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 {
                     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 {
                     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 {
                         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 {
                             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 {
                             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 {
                         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 {
                         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 {
                     HStack {
                         Button("Start new Profile") {
                         Button("Start new Profile") {
                             showAlert.toggle()
                             showAlert.toggle()
@@ -244,6 +354,7 @@ extension OverrideProfilesConfig {
                             .tint(.orange)
                             .tint(.orange)
                             .frame(maxWidth: .infinity, alignment: .trailing)
                             .frame(maxWidth: .infinity, alignment: .trailing)
                             .buttonStyle(BorderlessButtonStyle())
                             .buttonStyle(BorderlessButtonStyle())
+                            .font(.callout)
                             .controlSize(.mini)
                             .controlSize(.mini)
                             .disabled(unChanged())
                             .disabled(unChanged())
                     }
                     }
@@ -251,8 +362,6 @@ extension OverrideProfilesConfig {
                         presetPopover
                         presetPopover
                     }
                     }
                 }
                 }
-
-                header: { Text("Insulin") }
                 footer: {
                 footer: {
                     Text(
                     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."
                         "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")
             .navigationBarTitle("Profiles")
             .navigationBarTitleDisplayMode(.automatic)
             .navigationBarTitleDisplayMode(.automatic)
             .navigationBarItems(leading: Button("Close", action: state.hideModal))
             .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 {
         @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 {
                 HStack {
                     VStack {
                     VStack {
                         HStack {
                         HStack {
-                            Text(name)
+                            Text(data.name)
                             Spacer()
                             Spacer()
                         }
                         }
                         HStack(spacing: 5) {
                         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 {
                             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()
                             Spacer()
                         }
                         }
@@ -339,6 +449,39 @@ extension OverrideProfilesConfig {
             return defaultProfile || noDurationSpecified || targetZeroWithOverride || allSettingsDefault
             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) {
         private func removeProfile(at offsets: IndexSet) {
             for index in offsets {
             for index in offsets {
                 let language = fetchedProfiles[index]
                 let language = fetchedProfiles[index]

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

@@ -11,7 +11,11 @@ enum PreferencesEditor {
 
 
     enum FieldType {
     enum FieldType {
         case boolean(keypath: WritableKeyPath<Preferences, Bool>)
         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>)
         case insulinCurve(keypath: WritableKeyPath<Preferences, InsulinCurve>)
     }
     }
 
 
@@ -34,7 +38,7 @@ enum PreferencesEditor {
         var decimalValue: Decimal {
         var decimalValue: Decimal {
             get {
             get {
                 switch type {
                 switch type {
-                case let .decimal(keypath):
+                case let .decimal(keypath, _, _):
                     return settable?.get(keypath) ?? 0
                     return settable?.get(keypath) ?? 0
                 default: return 0
                 default: return 0
                 }
                 }
@@ -57,8 +61,20 @@ enum PreferencesEditor {
             switch (type, value) {
             switch (type, value) {
             case let (.boolean(keypath), value as Bool):
             case let (.boolean(keypath), value as Bool):
                 settable?.set(keypath, value: value)
                 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):
             case let (.insulinCurve(keypath), value as InsulinCurve):
                 settable?.set(keypath, value: value)
                 settable?.set(keypath, value: value)
             default: break
             default: break

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 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("mg/dL").tag(0)
                         Text("mmol/L").tag(1)
                         Text("mmol/L").tag(1)
                     }
                     }
-
-                    Toggle("Remote control", isOn: $state.allowAnnouncements)
-
                     HStack {
                     HStack {
                         Text("Recommended Bolus Percentage")
                         Text("Recommended Bolus Percentage")
                         DecimalTextField("", value: $state.insulinReqPercentage, formatter: formatter)
                         DecimalTextField("", value: $state.insulinReqPercentage, formatter: formatter)

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

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

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

@@ -19,7 +19,7 @@ extension PumpSettingsEditor {
         }
         }
 
 
         func save(settings: PumpSettings) -> AnyPublisher<Void, Error> {
         func save(settings: PumpSettings) -> AnyPublisher<Void, Error> {
-            func save() {
+            func save(_ settings: PumpSettings) {
                 storage.save(settings, as: OpenAPS.Settings.settings)
                 storage.save(settings, as: OpenAPS.Settings.settings)
                 processQueue.async {
                 processQueue.async {
                     self.broadcaster.notify(PumpSettingsObserver.self, on: self.processQueue) {
                     self.broadcaster.notify(PumpSettingsObserver.self, on: self.processQueue) {
@@ -29,7 +29,7 @@ extension PumpSettingsEditor {
             }
             }
 
 
             guard let pump = deviceManager?.pumpManager else {
             guard let pump = deviceManager?.pumpManager else {
-                save()
+                save(settings)
                 return Just(()).setFailureType(to: Error.self).eraseToAnyPublisher()
                 return Just(()).setFailureType(to: Error.self).eraseToAnyPublisher()
             }
             }
             // Don't ask why 🤦‍♂️
             // Don't ask why 🤦‍♂️
@@ -44,8 +44,21 @@ extension PumpSettingsEditor {
                 self.processQueue.async {
                 self.processQueue.async {
                     pump.syncDeliveryLimits(limits: limits) { result in
                     pump.syncDeliveryLimits(limits: limits) { result in
                         switch result {
                         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(()))
                             promise(.success(()))
                         case let .failure(error):
                         case let .failure(error):
                             promise(.failure(error))
                             promise(.failure(error))

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

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

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

@@ -1,5 +1,5 @@
 extension Settings {
 extension Settings {
     final class Provider: BaseProvider, SettingsProvider {
     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 debugOptions = false
         @Published var animatedBackground = false
         @Published var animatedBackground = false
         @Published var serviceUIType: ServiceUI.Type?
         @Published var serviceUIType: ServiceUI.Type?
-        @Published var setupTidePool = false
+        @Published var setupTidepool = false
 
 
         private(set) var buildNumber = ""
         private(set) var buildNumber = ""
         private(set) var versionNumber = ""
         private(set) var versionNumber = ""
@@ -62,6 +62,23 @@ extension Settings {
         func hideSettingsModal() {
         func hideSettingsModal() {
             hideModal()
             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 {
 extension Settings.StateModel: ServiceOnboardingDelegate {
     func serviceOnboarding(didCreateService service: Service) {
     func serviceOnboarding(didCreateService service: Service) {
         debug(.nightscout, "Service with identifier \(service.pluginIdentifier) created")
         debug(.nightscout, "Service with identifier \(service.pluginIdentifier) created")
-        provider.tidePoolManager.addTidePoolService(service: service)
+        provider.tidepoolManager.addTidepoolService(service: service)
     }
     }
 
 
     func serviceOnboarding(didOnboardService service: Service) {
     func serviceOnboarding(didOnboardService service: Service) {
@@ -86,7 +103,7 @@ extension Settings.StateModel: ServiceOnboardingDelegate {
 
 
 extension Settings.StateModel: CompletionDelegate {
 extension Settings.StateModel: CompletionDelegate {
     func completionNotifyingDidComplete(_: CompletionNotifying) {
     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 {
                 Section {
                     Text("Nightscout").navigationLink(to: .nighscoutConfig, from: self)
                     Text("Nightscout").navigationLink(to: .nighscoutConfig, from: self)
 
 
-                    Text("TidePool")
-                        .onTapGesture {
-                            state.setupTidePool = true
-                        }
+                    NavigationLink(destination: TidepoolStartView(state: state)) {
+                        Text("Tidepool")
+                    }
                     if HKHealthStore.isHealthDataAvailable() {
                     if HKHealthStore.isHealthDataAvailable() {
                         Text("Apple Health").navigationLink(to: .healthkit, from: self)
                         Text("Apple Health").navigationLink(to: .healthkit, from: self)
                     }
                     }
@@ -62,6 +61,16 @@ extension Settings {
                                     .frame(maxWidth: .infinity, alignment: .trailing)
                                     .frame(maxWidth: .infinity, alignment: .trailing)
                                     .buttonStyle(.borderedProminent)
                                     .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 {
                         Group {
                             Text("Preferences")
                             Text("Preferences")
@@ -130,26 +139,6 @@ extension Settings {
             .sheet(isPresented: $showShareSheet) {
             .sheet(isPresented: $showShareSheet) {
                 ShareSheet(activityItems: state.logItems())
                 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)
             .onAppear(perform: configureView)
             .navigationTitle("Settings")
             .navigationTitle("Settings")
             .navigationBarItems(leading: Button("Close", action: state.hideSettingsModal))
             .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 LoopKitUI
 import SwiftUI
 import SwiftUI
 
 
-struct TidePoolSetupView: UIViewControllerRepresentable {
+struct TidepoolSetupView: UIViewControllerRepresentable {
     let serviceUIType: ServiceUI.Type
     let serviceUIType: ServiceUI.Type
     let pluginHost: PluginHost
     let pluginHost: PluginHost
     let serviceOnBoardDelegate: ServiceOnboardingDelegate
     let serviceOnBoardDelegate: ServiceOnboardingDelegate
     let serviceDelegate: CompletionDelegate
     let serviceDelegate: CompletionDelegate
 
 
-    func makeUIViewController(context _: UIViewControllerRepresentableContext<TidePoolSetupView>) -> UIViewController {
+    func makeUIViewController(context _: UIViewControllerRepresentableContext<TidepoolSetupView>) -> UIViewController {
         let result = serviceUIType.setupViewController(
         let result = serviceUIType.setupViewController(
             colorPalette: .default,
             colorPalette: .default,
             pluginHost: pluginHost
             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 serviceUI: ServiceUI
     let serviceOnBoardDelegate: ServiceOnboardingDelegate
     let serviceOnBoardDelegate: ServiceOnboardingDelegate
     let serviceDelegate: CompletionDelegate?
     let serviceDelegate: CompletionDelegate?
 
 
-    func makeUIViewController(context _: UIViewControllerRepresentableContext<TidePoolSettingsView>) -> UIViewController {
+    func makeUIViewController(context _: UIViewControllerRepresentableContext<TidepoolSettingsView>) -> UIViewController {
         var vc = serviceUI.settingsViewController(colorPalette: .default)
         var vc = serviceUI.settingsViewController(colorPalette: .default)
         vc.completionDelegate = serviceDelegate
         vc.completionDelegate = serviceDelegate
         vc.serviceOnboardingDelegate = serviceOnBoardDelegate
         vc.serviceOnboardingDelegate = serviceOnBoardDelegate
         return vc
         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)
                             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")) {
                 Section(header: Text("Garmin Watch")) {
                     List {
                     List {
                         ForEach(state.devices, id: \.uuid) { device in
                         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 devices: [IQDevice] = []
         @Published var selectedAwConfig: AwConfig = .HR
         @Published var selectedAwConfig: AwConfig = .HR
         @Published var displayFatAndProteinOnWatch = false
         @Published var displayFatAndProteinOnWatch = false
+        @Published var confirmBolusFaster = false
 
 
         private(set) var preferences = Preferences()
         private(set) var preferences = Preferences()
 
 
@@ -38,6 +39,7 @@ extension WatchConfig {
             preferences = provider.preferences
             preferences = provider.preferences
 
 
             subscribeSetting(\.displayFatAndProteinOnWatch, on: $displayFatAndProteinOnWatch) { displayFatAndProteinOnWatch = $0 }
             subscribeSetting(\.displayFatAndProteinOnWatch, on: $displayFatAndProteinOnWatch) { displayFatAndProteinOnWatch = $0 }
+            subscribeSetting(\.confirmBolusFaster, on: $confirmBolusFaster) { confirmBolusFaster = $0 }
             subscribeSetting(\.displayOnWatch, on: $selectedAwConfig) { selectedAwConfig = $0 }
             subscribeSetting(\.displayOnWatch, on: $selectedAwConfig) { selectedAwConfig = $0 }
             didSet: { [weak self] value in
             didSet: { [weak self] value in
                 // for compatibility with old displayHR
                 // for compatibility with old displayHR

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

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

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

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

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

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

+ 9 - 5
FreeAPSTests/CalibrationsTests.swift

@@ -12,6 +12,9 @@ class CalibrationsTests: XCTestCase, Injectable {
     }
     }
 
 
     func testCreateSimpleCalibration() {
     func testCreateSimpleCalibration() {
+        // restore state so each test is independent
+        calibrationService.removeAllCalibrations()
+
         let calibration = Calibration(x: 100.0, y: 102.0)
         let calibration = Calibration(x: 100.0, y: 102.0)
         calibrationService.addCalibration(calibration)
         calibrationService.addCalibration(calibration)
 
 
@@ -25,17 +28,18 @@ class CalibrationsTests: XCTestCase, Injectable {
     }
     }
 
 
     func testCreateMultipleCalibration() {
     func testCreateMultipleCalibration() {
+        // restore state so each test is independent
+        calibrationService.removeAllCalibrations()
+
         let calibration = Calibration(x: 100.0, y: 120)
         let calibration = Calibration(x: 100.0, y: 120)
         calibrationService.addCalibration(calibration)
         calibrationService.addCalibration(calibration)
 
 
         let calibration2 = Calibration(x: 120.0, y: 130.0)
         let calibration2 = Calibration(x: 120.0, y: 130.0)
         calibrationService.addCalibration(calibration2)
         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()
         calibrationService.removeLast()
 
 

+ 1 - 0
FreeAPSWatch WatchKit Extension/DataFlow.swift

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

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

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

+ 2 - 0
FreeAPSWatch WatchKit Extension/WatchStateModel.swift

@@ -34,6 +34,7 @@ class WatchStateModel: NSObject, ObservableObject {
     @Published var isBolusViewActive = false
     @Published var isBolusViewActive = false
     @Published var displayOnWatch: AwConfig = .BGTarget
     @Published var displayOnWatch: AwConfig = .BGTarget
     @Published var displayFatAndProteinOnWatch = false
     @Published var displayFatAndProteinOnWatch = false
+    @Published var confirmBolusFaster = false
     @Published var eventualBG = ""
     @Published var eventualBG = ""
     @Published var isConfirmationViewActive = false {
     @Published var isConfirmationViewActive = false {
         didSet {
         didSet {
@@ -174,6 +175,7 @@ class WatchStateModel: NSObject, ObservableObject {
         eventualBG = state.eventualBG ?? ""
         eventualBG = state.eventualBG ?? ""
         displayOnWatch = state.displayOnWatch ?? .BGTarget
         displayOnWatch = state.displayOnWatch ?? .BGTarget
         displayFatAndProteinOnWatch = state.displayFatAndProteinOnWatch ?? false
         displayFatAndProteinOnWatch = state.displayFatAndProteinOnWatch ?? false
+        confirmBolusFaster = state.confirmBolusFaster ?? false
         isf = state.isf
         isf = state.isf
         override = state.override
         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)
                     label.fontWidth(.compressed)
                 }
                 }
             }
             }
-            .widgetURL(URL(string: "freeaps-x://"))
+            .widgetURL(URL(string: "Trio://"))
             .keylineTint(Color.purple)
             .keylineTint(Color.purple)
             .contentMargins(.horizontal, 0, for: .minimal)
             .contentMargins(.horizontal, 0, for: .minimal)
             .contentMargins(.trailing, 0, for: .compactLeading)
             .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

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 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:
 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)
 e023125 Replace Open-iAPS with Trio (#23)
 fa373c9 Merge pull request #22 from nightscout/tmhastings-tddAdjBasal
 fa373c9 Merge pull request #22 from nightscout/tmhastings-tddAdjBasal
 fc0ae69 tddAdjBasal pop-up correction
 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
     , 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: 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.
     , 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
   }
   }
 }
 }