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

Merge branch 'core-data-sync-trio' into auggie-dynamic-bg-color

Deniz Cengiz 1 год назад
Родитель
Сommit
12539a57c2
62 измененных файлов с 2837 добавлено и 1374 удалено
  1. 17 5
      .github/workflows/add_identifiers.yml
  2. 179 147
      .github/workflows/build_trio.yml
  3. 16 5
      .github/workflows/create_certs.yml
  4. 10 7
      .github/workflows/validate_secrets.yml
  5. 20 0
      FreeAPS.xcodeproj/project.pbxproj
  6. 1 1
      FreeAPS/Resources/javascript/bundle/autosens.js
  7. 1 1
      FreeAPS/Resources/javascript/bundle/autotune-prep.js
  8. 1 1
      FreeAPS/Resources/javascript/bundle/iob.js
  9. 1 1
      FreeAPS/Resources/javascript/bundle/meal.js
  10. 1 1
      FreeAPS/Resources/javascript/bundle/profile.js
  11. 1 0
      FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json
  12. 56 20
      FreeAPS/Sources/APS/APSManager.swift
  13. 25 6
      FreeAPS/Sources/APS/CGM/PluginSource.swift
  14. 0 1
      FreeAPS/Sources/APS/DeviceDataManager.swift
  15. 18 16
      FreeAPS/Sources/APS/FetchGlucoseManager.swift
  16. 145 52
      FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift
  17. 10 2
      FreeAPS/Sources/APS/Storage/CarbsStorage.swift
  18. 22 22
      FreeAPS/Sources/APS/Storage/DeterminationStorage.swift
  19. 15 7
      FreeAPS/Sources/APS/Storage/GlucoseStorage.swift
  20. 16 6
      FreeAPS/Sources/APS/Storage/OverrideStorage.swift
  21. 3 1
      FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift
  22. 8 0
      FreeAPS/Sources/Helpers/Rounding.swift
  23. 28 5
      FreeAPS/Sources/Models/BloodGlucose.swift
  24. 5 0
      FreeAPS/Sources/Models/FreeAPSSettings.swift
  25. 8 8
      FreeAPS/Sources/Modules/AutotuneConfig/View/AutotuneConfigRootView.swift
  26. 5 1
      FreeAPS/Sources/Modules/Bolus/BolusDataFlow.swift
  27. 26 4
      FreeAPS/Sources/Modules/Bolus/BolusProvider.swift
  28. 308 192
      FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift
  29. 119 0
      FreeAPS/Sources/Modules/Bolus/View/AddMealPresetView.swift
  30. 129 245
      FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift
  31. 255 0
      FreeAPS/Sources/Modules/Bolus/View/ForeCastChart.swift
  32. 333 0
      FreeAPS/Sources/Modules/Bolus/View/MealPresetView.swift
  33. 5 0
      FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift
  34. 30 20
      FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift
  35. 8 0
      FreeAPS/Sources/Modules/HealthKit/View/AppleHealthKitRootView.swift
  36. 216 170
      FreeAPS/Sources/Modules/Home/HomeStateModel.swift
  37. 321 90
      FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift
  38. 9 14
      FreeAPS/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift
  39. 44 32
      FreeAPS/Sources/Modules/Home/View/HomeRootView.swift
  40. 3 14
      FreeAPS/Sources/Modules/OverrideConfig/View/OverrideRootView.swift
  41. 27 23
      FreeAPS/Sources/Modules/Stat/StatStateModel.swift
  42. 2 0
      FreeAPS/Sources/Modules/StatConfig/StatConfigStateModel.swift
  43. 1 0
      FreeAPS/Sources/Modules/StatConfig/View/StatConfigRootView.swift
  44. 9 3
      FreeAPS/Sources/Services/Calendar/CalendarManager.swift
  45. 3 32
      FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift
  46. 33 9
      FreeAPS/Sources/Services/LiveActivity/Data/DataManager.swift
  47. 3 3
      FreeAPS/Sources/Services/LiveActivity/LiveActitiyAttributes.swift
  48. 8 20
      FreeAPS/Sources/Services/LiveActivity/LiveActivityAttributes+Helper.swift
  49. 11 11
      FreeAPS/Sources/Services/Network/NightscoutAPI.swift
  50. 0 14
      FreeAPS/Sources/Services/Network/NightscoutManager.swift
  51. 5 2
      FreeAPS/Sources/Services/UserNotifications/UserNotificationsManager.swift
  52. 117 101
      FreeAPS/Sources/Services/WatchManager/WatchManager.swift
  53. 28 2
      FreeAPS/Sources/Views/TextFieldWithToolBar.swift
  54. 62 9
      LiveActivity/LiveActivity.swift
  55. 44 20
      Model/CoreDataStack.swift
  56. 17 0
      Model/Helper/CarbsGlucose+helper.swift
  57. 18 0
      Model/Helper/NSPredicates.swift
  58. 8 3
      Model/Helper/PumpEvent+helper.swift
  59. 3 2
      Trio.xcworkspace/xcshareddata/swiftpm/Package.resolved
  60. 4 1
      oref0_source_version.txt
  61. 1 6
      scripts/capture-build-details.sh
  62. 15 16
      trio-oref/lib/profile/index.js

+ 17 - 5
.github/workflows/add_identifiers.yml

@@ -10,12 +10,13 @@ jobs:
     secrets: inherit
 
   identifiers:
+    name: Add Identifiers
     needs: validate
-    runs-on: macos-13
+    runs-on: macos-14
     steps:
-      # Uncomment to manually select Xcode version if needed
-      #- name: Select Xcode version
-      #  run: "sudo xcode-select --switch /Applications/Xcode_15.0.1.app/Contents/Developer"
+      # Uncomment to manually select latest Xcode if needed
+      #- name: Select Latest Xcode
+      #  run: "sudo xcode-select --switch /Applications/Xcode_13.0.app/Contents/Developer"
 
       # Checks-out the repo
       - name: Checkout Repo
@@ -23,12 +24,23 @@ jobs:
 
       # Patch Fastlane Match to not print tables
       - name: Patch Match Tables
-        run: find /usr/local/lib/ruby/gems -name table_printer.rb | xargs sed -i "" "/puts(Terminal::Table.new(params))/d"
+        run: |
+          TABLE_PRINTER_PATH=$(ruby -e 'puts Gem::Specification.find_by_name("fastlane").gem_dir')/match/lib/match/table_printer.rb
+          if [ -f "$TABLE_PRINTER_PATH" ]; then
+            sed -i "" "/puts(Terminal::Table.new(params))/d" "$TABLE_PRINTER_PATH"
+          else
+            echo "table_printer.rb not found"
+            exit 1
+          fi
 
       # Install project dependencies
       - name: Install Project Dependencies
         run: bundle install
 
+      # Sync the GitHub runner clock with the Windows time server (workaround as suggested in https://github.com/actions/runner/issues/2996)
+      - name: Sync clock
+        run: sudo sntp -sS time.windows.com
+
       # Create or update identifiers for app
       - name: Fastlane Provision
         run: bundle exec fastlane identifiers

+ 179 - 147
.github/workflows/build_trio.yml

@@ -2,20 +2,20 @@ name: 4. Build Trio
 run-name: Build Trio (${{ github.ref_name }})
 on:
   workflow_dispatch:
-  
+
   ## Remove the "#" sign from the beginning of the line below to get automated builds on push (code changes in your repository)
   #push:
-  
+
   schedule:
-    #- cron: '30 04 1 * *' # Runs at 04:30 UTC on the 1st every month
-    - cron: '0 8 * * 3' # Checks for updates at 08:00 UTC every Wednesday
-    - cron: '0 6 1 * *' # Builds the app on the 1st of every month at 06:00 UTC
+    - cron: "0 8 * * 3" # Checks for updates at 08:00 UTC every Wednesday
+    - cron: "0 6 1 * *" # Builds the app on the 1st of every month at 06:00 UTC
 
 env:  
   UPSTREAM_REPO: nightscout/Trio
   UPSTREAM_BRANCH: ${{ github.ref_name }} # branch on upstream repository to sync from (replace with specific branch name if needed)
   TARGET_BRANCH: ${{ github.ref_name }} # target branch on fork to be kept in sync, and target branch on upstream to be kept alive (replace with specific branch name if needed)
-  ALIVE_BRANCH: alive
+  ALIVE_BRANCH_MAIN: alive-main
+  ALIVE_BRANCH_DEV: alive-dev
 
 jobs:
   validate:
@@ -33,151 +33,172 @@ jobs:
       contents: write
     outputs:
       WORKFLOW_PERMISSION: ${{ steps.workflow-permission.outputs.has_permission }}
-    
+
     steps:
-    - name: Check for workflow permissions
-      id: workflow-permission
-      env: 
-        TOKEN_TO_CHECK: ${{ secrets.GH_PAT }}
-      run: |
-        PERMISSIONS=$(curl -sS -f -I -H "Authorization: token ${{ env.TOKEN_TO_CHECK }}" https://api.github.com | grep ^x-oauth-scopes: | cut -d' ' -f2-);
-        
-        if [[ $PERMISSIONS =~ "workflow" || $PERMISSIONS == "" ]]; then
-          echo "GH_PAT holds workflow permissions or is fine-grained PAT."
-          echo "has_permission=true" >> $GITHUB_OUTPUT # Set WORKFLOW_PERMISSION to false.
-        else 
-          echo "GH_PAT lacks workflow permissions."
-          echo "Automated build features will be skipped!"
-          echo "has_permission=false" >> $GITHUB_OUTPUT # Set WORKFLOW_PERMISSION to false.
-        fi
-    
-    - name: Check for alive branch
-      if: steps.workflow-permission.outputs.has_permission == 'true'
-      env:
-        GITHUB_TOKEN: ${{ secrets.GH_PAT }}
-      run: |
-        if [[ "$(gh api -H "Accept: application/vnd.github+json" /repos/${{ github.repository }}/branches | jq --raw-output 'any(.name=="alive")')" == "true" ]]; then
-          echo "Branch 'alive' exists."
-          echo "ALIVE_BRANCH_EXISTS=true" >> $GITHUB_ENV # Set ALIVE_BRANCH_EXISTS to true
-        else
-          echo "Branch 'alive' does not exist."
-          echo "ALIVE_BRANCH_EXISTS=false" >> $GITHUB_ENV # Set ALIVE_BRANCH_EXISTS to false
-        fi
-    
-    - name: Create alive branch
-      if: env.ALIVE_BRANCH_EXISTS == 'false'
-      env:
-        GITHUB_TOKEN: ${{ secrets.GH_PAT }}
-      run: |
-        # get ref for nightscout/Trio:dev
-        response=$(curl --request GET \
-                          --url "https://api.github.com/repos/${{ env.UPSTREAM_REPO }}/git/refs/heads/dev" \
-                          --header "Authorization: Bearer $GITHUB_TOKEN" \
-                          --silent)
-        echo "API Response: $response"
-        SHA=$(echo "$response" | jq -r '.object.sha')
-        if [ "$SHA" = "null" ]; then
-            echo "Error: Unable to retrieve SHA for the dev branch."
-            exit 1
-        fi
-        echo "SHA of dev branch: $SHA";
-        
-        # Create alive branch based on nightscout/Trio:dev
-        gh api \
-          --method POST \
-          -H "Authorization: token $GITHUB_TOKEN" \
-          -H "Accept: application/vnd.github.v3+json" \
-          /repos/${{ github.repository }}/git/refs \
-          -f ref='refs/heads/alive' \
-          -f sha=$SHA
-  
+      - name: Check for workflow permissions
+        id: workflow-permission
+        env:
+          TOKEN_TO_CHECK: ${{ secrets.GH_PAT }}
+        run: |
+          PERMISSIONS=$(curl -sS -f -I -H "Authorization: token ${{ env.TOKEN_TO_CHECK }}" https://api.github.com | grep ^x-oauth-scopes: | cut -d' ' -f2-);
+
+          if [[ $PERMISSIONS =~ "workflow" || $PERMISSIONS == "" ]]; then
+            echo "GH_PAT holds workflow permissions or is fine-grained PAT."
+            echo "has_permission=true" >> $GITHUB_OUTPUT # Set WORKFLOW_PERMISSION to false.
+          else 
+            echo "GH_PAT lacks workflow permissions."
+            echo "Automated build features will be skipped!"
+            echo "has_permission=false" >> $GITHUB_OUTPUT # Set WORKFLOW_PERMISSION to false.
+          fi
+
+      - name: Check for alive branches
+        if: steps.workflow-permission.outputs.has_permission == 'true'
+        env:
+          GITHUB_TOKEN: ${{ secrets.GH_PAT }}
+        run: |
+          if [[ $(gh api -H "Accept: application/vnd.github+json" /repos/${{ github.repository_owner }}/Trio/branches | jq --raw-output '[.[] | select(.name == "alive-main" or .name == "alive-dev")] | length > 0') == "true" ]]; then
+            echo "Branches 'alive-main' or 'alive-dev' exist."
+            echo "ALIVE_BRANCH_EXISTS=true" >> $GITHUB_ENV
+          else
+            echo "Branches 'alive-main' and 'alive-dev' do not exist."
+            echo "ALIVE_BRANCH_EXISTS=false" >> $GITHUB_ENV
+          fi
+
+      - name: Create alive branches
+        if: env.ALIVE_BRANCH_EXISTS == 'false'
+        env:
+          GITHUB_TOKEN: ${{ secrets.GH_PAT }}
+        run: |
+          # Get ref for UPSTREAM_REPO:main
+          SHA_MAIN=$(curl -sS -H "Authorization: token $GITHUB_TOKEN" https://api.github.com/repos/${{ env.UPSTREAM_REPO }}/git/refs/heads/main | jq -r '.object.sha')
+
+          # Get ref for UPSTREAM_REPO:dev
+          SHA_DEV=$(curl -sS -H "Authorization: token $GITHUB_TOKEN" https://api.github.com/repos/${{ env.UPSTREAM_REPO }}/git/refs/heads/dev | jq -r '.object.sha')
+
+          # Create alive-main branch in Trio fork based on UPSTREAM_REPO:main
+          gh api \
+            --method POST \
+            -H "Authorization: token $GITHUB_TOKEN" \
+            -H "Accept: application/vnd.github.v3+json" \
+            /repos/${{ github.repository_owner }}/Trio/git/refs \
+            -f ref='refs/heads/alive-main' \
+            -f sha=$SHA_MAIN
+
+          # Create alive-dev branch in Trio fork based on UPSTREAM_REPO:dev
+          gh api \
+            --method POST \
+            -H "Authorization: token $GITHUB_TOKEN" \
+            -H "Accept: application/vnd.github.v3+json" \
+            /repos/${{ github.repository_owner }}/Trio/git/refs \
+            -f ref='refs/heads/alive-dev' \
+            -f sha=$SHA_DEV
+
   # Checks for changes in upstream repository; if changes exist prompts sync for build
   # Performs keepalive to avoid stale fork
   check_latest_from_upstream:
     needs: [validate, check_alive_and_permissions]
     runs-on: ubuntu-latest
     name: Check upstream and keep alive
-    outputs: 
+    outputs:
       NEW_COMMITS: ${{ steps.sync.outputs.has_new_commits }}
-    
+      ABORT_SYNC: ${{ steps.check_branch.outputs.ABORT_SYNC }}
+
     steps:
-    - name: Checkout target repo
-      if: |
-        needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' &&
-        (vars.SCHEDULED_BUILD != 'false' || vars.SCHEDULED_SYNC != 'false')
-      uses: actions/checkout@v4
-      with:
-        token: ${{ secrets.GH_PAT }}
-        ref: alive
-    
-    - name: Sync upstream changes
-      if: | # do not run the upstream sync action on the upstream repository
-        needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' &&
-        vars.SCHEDULED_SYNC != 'false' && github.repository_owner != 'nightscout'
-      id: sync
-      uses: aormsby/Fork-Sync-With-Upstream-action@v3.4.1
-      with:
-        target_sync_branch: ${{ env.ALIVE_BRANCH }}
-        shallow_since: 6 months ago
-        target_repo_token: ${{ secrets.GH_PAT }}
-        upstream_sync_branch: ${{ env.UPSTREAM_BRANCH }}
-        upstream_sync_repo: ${{ env.UPSTREAM_REPO }}
-    
-    # Display a sample message based on the sync output var 'has_new_commits'
-    - name: New commits found
-      if: |
-        needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' &&
-        vars.SCHEDULED_SYNC != 'false' && steps.sync.outputs.has_new_commits == 'true'
-      run: echo "New commits were found to sync."
-    
-    - name: No new commits
-      if: |
-        needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' && 
-        vars.SCHEDULED_SYNC != 'false' && steps.sync.outputs.has_new_commits == 'false'
-      run: echo "There were no new commits."
-    
-    - name: Show value of 'has_new_commits'
-      if: needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' && vars.SCHEDULED_SYNC != 'false'
-      run: |
-        echo ${{ steps.sync.outputs.has_new_commits }}
-        echo "NEW_COMMITS=${{ steps.sync.outputs.has_new_commits }}" >> $GITHUB_OUTPUT
-    
-    # Keep repository "alive": add empty commits to ALIVE_BRANCH after "time_elapsed" days of inactivity to avoid inactivation of scheduled workflows
-    - name: Keep alive
-      if: |
-        needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' &&
-        (vars.SCHEDULED_BUILD != 'false' || vars.SCHEDULED_SYNC != 'false')
-      uses: gautamkrishnar/keepalive-workflow@v1 # using the workflow with default settings
-      with:
-        time_elapsed: 20 # Time elapsed from the previous commit to trigger a new automated commit (in days)
-    
-    - name: Show scheduled build configuration message
-      if: needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION != 'true'
-      run: |
-        echo "### :calendar: Scheduled Sync and Build Disabled :mobile_phone_off:" >> $GITHUB_STEP_SUMMARY
-        echo "You have not yet configured the scheduled sync and build for Trio's browser build." >> $GITHUB_STEP_SUMMARY
-        echo "Synchronizing your fork of <code>Trio</code> with the upstream repository <code>nightscout/Trio</code> will be skipped." >> $GITHUB_STEP_SUMMARY
-        echo "If you want to enable automatic builds and updates for your Trio, please follow the instructions \
+      - name: Check if running on main or dev branch
+        if: |
+          needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' &&
+          (vars.SCHEDULED_BUILD != 'false' || vars.SCHEDULED_SYNC != 'false')
+        id: check_branch
+        run: |
+          if [ "${GITHUB_REF##*/}" = "main" ]; then
+            echo "Running on main branch"
+            echo "ALIVE_BRANCH=${ALIVE_BRANCH_MAIN}" >> $GITHUB_OUTPUT
+            echo "ABORT_SYNC=false" >> $GITHUB_OUTPUT
+          elif [ "${GITHUB_REF##*/}" = "dev" ]; then
+            echo "Running on dev branch"
+            echo "ALIVE_BRANCH=${ALIVE_BRANCH_DEV}" >> $GITHUB_OUTPUT
+            echo "ABORT_SYNC=false" >> $GITHUB_OUTPUT
+          else
+            echo "Not running on main or dev branch"
+            echo "ABORT_SYNC=true" >> $GITHUB_OUTPUT
+          fi
+
+      - name: Checkout target repo
+        if: |
+          needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' &&
+          (vars.SCHEDULED_BUILD != 'false' || vars.SCHEDULED_SYNC != 'false')
+        uses: actions/checkout@v4
+        with:
+          token: ${{ secrets.GH_PAT }}
+          ref: ${{ steps.check_branch.outputs.ALIVE_BRANCH }}
+
+      - name: Sync upstream changes
+        if: | # do not run the upstream sync action on the upstream repository
+          needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' &&
+          vars.SCHEDULED_SYNC != 'false' && github.repository_owner != 'nightscout' && steps.check_branch.outputs.ABORT_SYNC == 'false'
+        id: sync
+        uses: aormsby/Fork-Sync-With-Upstream-action@v3.4.1
+        with:
+          target_sync_branch: ${{ steps.check_branch.outputs.ALIVE_BRANCH }}
+          shallow_since: 6 months ago
+          target_repo_token: ${{ secrets.GH_PAT }}
+          upstream_sync_branch: ${{ env.UPSTREAM_BRANCH }}
+          upstream_sync_repo: ${{ env.UPSTREAM_REPO }}
+
+      # Display a sample message based on the sync output var 'has_new_commits'
+      - name: New commits found
+        if: |
+          needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' &&
+          vars.SCHEDULED_SYNC != 'false' && steps.sync.outputs.has_new_commits == 'true'
+        run: echo "New commits were found to sync."
+
+      - name: No new commits
+        if: |
+          needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' && 
+          vars.SCHEDULED_SYNC != 'false' && steps.sync.outputs.has_new_commits == 'false'
+        run: echo "There were no new commits."
+
+      - name: Show value of 'has_new_commits'
+        if: needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' && vars.SCHEDULED_SYNC != 'false' && steps.check_branch.outputs.ABORT_SYNC == 'false'
+        run: |
+          echo ${{ steps.sync.outputs.has_new_commits }}
+          echo "NEW_COMMITS=${{ steps.sync.outputs.has_new_commits }}" >> $GITHUB_OUTPUT
+
+      # Keep repository "alive": add empty commits to ALIVE_BRANCH after "time_elapsed" days of inactivity to avoid inactivation of scheduled workflows
+      - name: Keep alive
+        if: |
+          needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' &&
+          (vars.SCHEDULED_BUILD != 'false' || vars.SCHEDULED_SYNC != 'false')
+        uses: gautamkrishnar/keepalive-workflow@v1 # using the workflow with default settings
+        with:
+          time_elapsed: 20 # Time elapsed from the previous commit to trigger a new automated commit (in days)
+
+      - name: Show scheduled build configuration message
+        if: needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION != 'true'
+        run: |
+          echo "### :calendar: Scheduled Sync and Build Disabled :mobile_phone_off:" >> $GITHUB_STEP_SUMMARY
+          echo "You have not yet configured the scheduled sync and build for Trio's browser build." >> $GITHUB_STEP_SUMMARY
+          echo "Synchronizing your fork of <code>Trio</code> with the upstream repository <code>nightscout/Trio</code> will be skipped." >> $GITHUB_STEP_SUMMARY
+          echo "If you want to enable automatic builds and updates for your Trio, please follow the instructions \
               under the following path <code>Trio/fastlane/testflight.md</code>." >> $GITHUB_STEP_SUMMARY
-   
   
   # Builds Trio
   build:
     name: Build
     needs: [validate, check_alive_and_permissions, check_latest_from_upstream]
-    runs-on: macos-13
+    runs-on: macos-14
     permissions:
       contents: write
-    if: | # runs if started manually, or if sync schedule is set and enabled and scheduled on the first Saturday each month, or if sync schedule is set and enabled and new commits were found
-        github.event_name == 'workflow_dispatch' ||
-        (needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' &&
-          (vars.SCHEDULED_BUILD != 'false' && github.event.schedule == '0 6 1 * *') ||
-          (vars.SCHEDULED_SYNC != 'false' && needs.check_latest_from_upstream.outputs.NEW_COMMITS == 'true' )
-        )
+    if:
+      | # runs if started manually, or if sync schedule is set and enabled and scheduled on the first Saturday each month, or if sync schedule is set and enabled and new commits were found
+      github.event_name == 'workflow_dispatch' ||
+      (needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' &&
+        (vars.SCHEDULED_BUILD != 'false' && github.event.schedule == '0 6 1 * *') ||
+        (vars.SCHEDULED_SYNC != 'false' && needs.check_latest_from_upstream.outputs.NEW_COMMITS == 'true' )
+      )
     steps:
-      # Uncomment to manually select Xcode version if needed
-      #- name: Select Xcode version
-      #  run: "sudo xcode-select --switch /Applications/Xcode_15.0.1.app/Contents/Developer"
+      - name: Select Xcode version
+        run: "sudo xcode-select --switch /Applications/Xcode_15.4.app/Contents/Developer"
       
       - name: Checkout Repo for syncing
         if: |
@@ -186,12 +207,12 @@ jobs:
         uses: actions/checkout@v4
         with:
           token: ${{ secrets.GH_PAT }}
-          ref: ${{ env.TARGET_BRANCH }} 
-      
+          ref: ${{ env.TARGET_BRANCH }}
+
       - name: Sync upstream changes
         if: | # do not run the upstream sync action on the upstream repository
           needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' &&
-          vars.SCHEDULED_SYNC != 'false' && github.repository_owner != 'nightscout'
+          vars.SCHEDULED_SYNC != 'false' && github.repository_owner != 'nightscout' && needs.check_latest_from_upstream.outputs.ABORT_SYNC == 'false'
         id: sync
         uses: aormsby/Fork-Sync-With-Upstream-action@v3.4.1
         with:
@@ -200,24 +221,24 @@ jobs:
           target_repo_token: ${{ secrets.GH_PAT }}
           upstream_sync_branch: ${{ env.UPSTREAM_BRANCH }}
           upstream_sync_repo: ${{ env.UPSTREAM_REPO }}
-      
+
       # Display a sample message based on the sync output var 'has_new_commits'
       - name: New commits found
         if: |
           needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' &&
-          vars.SCHEDULED_SYNC != 'false' && steps.sync.outputs.has_new_commits == 'true'
+          vars.SCHEDULED_SYNC != 'false' && steps.sync.outputs.has_new_commits == 'true' && needs.check_latest_from_upstream.outputs.ABORT_SYNC == 'false'
         run: echo "New commits were found to sync."
-    
+
       - name: No new commits
         if: |
           needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' && 
-          vars.SCHEDULED_SYNC != 'false' && steps.sync.outputs.has_new_commits == 'false'
+          vars.SCHEDULED_SYNC != 'false' && steps.sync.outputs.has_new_commits == 'false' && needs.check_latest_from_upstream.outputs.ABORT_SYNC == 'false'
         run: echo "There were no new commits."
-      
+
       - name: Show value of 'has_new_commits'
         if: |
           needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true'
-          && vars.SCHEDULED_SYNC != 'false'
+          && vars.SCHEDULED_SYNC != 'false' && needs.check_latest_from_upstream.outputs.ABORT_SYNC == 'false'
         run: |
           echo ${{ steps.sync.outputs.has_new_commits }}
           echo "NEW_COMMITS=${{ steps.sync.outputs.has_new_commits }}" >> $GITHUB_OUTPUT
@@ -231,12 +252,23 @@ jobs:
 
       # Patch Fastlane Match to not print tables
       - name: Patch Match Tables
-        run: find /usr/local/lib/ruby/gems -name table_printer.rb | xargs sed -i "" "/puts(Terminal::Table.new(params))/d"
-      
+        run: |
+          TABLE_PRINTER_PATH=$(ruby -e 'puts Gem::Specification.find_by_name("fastlane").gem_dir')/match/lib/match/table_printer.rb
+          if [ -f "$TABLE_PRINTER_PATH" ]; then
+            sed -i "" "/puts(Terminal::Table.new(params))/d" "$TABLE_PRINTER_PATH"
+          else
+            echo "table_printer.rb not found"
+            exit 1
+          fi
+
       # Install project dependencies
-      - name: Install project dependencies
+      - name: Install Project Dependencies
         run: bundle install
-      
+
+      # Sync the GitHub runner clock with the Windows time server (workaround as suggested in https://github.com/actions/runner/issues/2996)
+      - name: Sync clock
+        run: sudo sntp -sS time.windows.com
+
       # Build signed Trio IPA file
       - name: Fastlane Build & Archive
         run: bundle exec fastlane build_trio
@@ -247,7 +279,7 @@ jobs:
           FASTLANE_ISSUER_ID: ${{ secrets.FASTLANE_ISSUER_ID }}
           FASTLANE_KEY: ${{ secrets.FASTLANE_KEY }}
           MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
-      
+
       # Upload to TestFlight
       - name: Fastlane upload to TestFlight
         run: bundle exec fastlane release

+ 16 - 5
.github/workflows/create_certs.yml

@@ -12,11 +12,11 @@ jobs:
   certificates:
     name: Create Certificates
     needs: validate
-    runs-on: macos-13
+    runs-on: macos-14
     steps:
-      # Uncomment to manually select Xcode version if needed
-      - name: Select Xcode version
-        run: "sudo xcode-select --switch /Applications/Xcode_15.0.1.app/Contents/Developer"
+      # Uncomment to manually select latest Xcode if needed
+      #- name: Select Latest Xcode
+      #  run: "sudo xcode-select --switch /Applications/Xcode_13.0.app/Contents/Developer"
 
       # Checks-out the repo
       - name: Checkout Repo
@@ -24,12 +24,23 @@ jobs:
 
       # Patch Fastlane Match to not print tables
       - name: Patch Match Tables
-        run: find /usr/local/lib/ruby/gems -name table_printer.rb | xargs sed -i "" "/puts(Terminal::Table.new(params))/d"
+        run: |
+          TABLE_PRINTER_PATH=$(ruby -e 'puts Gem::Specification.find_by_name("fastlane").gem_dir')/match/lib/match/table_printer.rb
+          if [ -f "$TABLE_PRINTER_PATH" ]; then
+            sed -i "" "/puts(Terminal::Table.new(params))/d" "$TABLE_PRINTER_PATH"
+          else
+            echo "table_printer.rb not found"
+            exit 1
+          fi
 
       # Install project dependencies
       - name: Install Project Dependencies
         run: bundle install
 
+      # Sync the GitHub runner clock with the Windows time server (workaround as suggested in https://github.com/actions/runner/issues/2996)
+      - name: Sync clock
+        run: sudo sntp -sS time.windows.com
+
       # Create or update certificates for app
       - name: Create Certificates
         run: bundle exec fastlane certs

+ 10 - 7
.github/workflows/validate_secrets.yml

@@ -5,7 +5,7 @@ on: [workflow_call, workflow_dispatch]
 jobs:
   validate-access-token:
     name: Access
-    runs-on: macos-13
+    runs-on: macos-14
     env:
       GH_PAT: ${{ secrets.GH_PAT }}
       GH_TOKEN: ${{ secrets.GH_PAT }}
@@ -74,7 +74,7 @@ jobs:
   validate-match-secrets:
     name: Match-Secrets
     needs: validate-access-token
-    runs-on: macos-13
+    runs-on: macos-14
     env:
       GH_TOKEN: ${{ secrets.GH_PAT }}
     steps:
@@ -112,7 +112,7 @@ jobs:
   validate-fastlane-secrets:
     name: Fastlane
     needs: [validate-access-token, validate-match-secrets]
-    runs-on: macos-13
+    runs-on: macos-14
     env:
       GH_PAT: ${{ secrets.GH_PAT }}
       GH_TOKEN: ${{ secrets.GH_PAT }}
@@ -125,10 +125,13 @@ jobs:
       - name: Checkout Repo
         uses: actions/checkout@v4
 
-      # Install project dependencies
       - name: Install Project Dependencies
         run: bundle install
 
+      # Sync the GitHub runner clock with the Windows time server (workaround as suggested in https://github.com/actions/runner/issues/2996)
+      - name: Sync clock
+        run: sudo sntp -sS time.windows.com
+
       - name: Validate Fastlane Secrets
         run: |
           # Validate Fastlane Secrets
@@ -165,13 +168,13 @@ jobs:
             [ -z "$FASTLANE_KEY"       ] && echo "::error::The FASTLANE_KEY secret is unset or empty. Set it and try again."
           elif [ ${#FASTLANE_KEY_ID} -ne 10 ]; then
             failed=true
-            echo "::error::The FASTLANE_KEY_ID secret is set but has wrong length. Verify that you copied it correctly from the 'Keys' tab at https://appstoreconnect.apple.com/access/api and try again."
+            echo "::error::The FASTLANE_KEY_ID secret is set but has wrong length. Verify that you copied it correctly from the 'Keys' tab at https://appstoreconnect.apple.com/access/integrations/api and try again."
           elif ! [[ $FASTLANE_KEY_ID =~ $FASTLANE_KEY_ID_PATTERN ]]; then
             failed=true
-            echo "::error::The FASTLANE_KEY_ID secret is set but invalid. Verify that you copied it correctly from the 'Keys' tab at https://appstoreconnect.apple.com/access/api and try again."
+            echo "::error::The FASTLANE_KEY_ID secret is set but invalid. Verify that you copied it correctly from the 'Keys' tab at https://appstoreconnect.apple.com/access/integrations/api and try again."
           elif ! [[ $FASTLANE_ISSUER_ID =~ $FASTLANE_ISSUER_ID_PATTERN ]]; then
             failed=true
-            echo "::error::The FASTLANE_ISSUER_ID secret is set but invalid. Verify that you copied it correctly from the 'Keys' tab at https://appstoreconnect.apple.com/access/api and try again."
+            echo "::error::The FASTLANE_ISSUER_ID secret is set but invalid. Verify that you copied it correctly from the 'Keys' tab at https://appstoreconnect.apple.com/access/integrations/api and try again."
           elif ! echo "$FASTLANE_KEY" | openssl pkcs8 -nocrypt >/dev/null; then
             failed=true
             echo "::error::The FASTLANE_KEY secret is set but invalid. Verify that you copied it correctly from the API Key file (*.p8) you downloaded and try again."

+ 20 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -299,6 +299,7 @@
 		B958F1B72BA0711600484851 /* MKRingProgressView in Frameworks */ = {isa = PBXBuildFile; productRef = B958F1B62BA0711600484851 /* MKRingProgressView */; };
 		B9CAAEFC2AE70836000F68BC /* branch.txt in Resources */ = {isa = PBXBuildFile; fileRef = B9CAAEFB2AE70836000F68BC /* branch.txt */; };
 		BA00D96F7B2FF169A06FB530 /* CGMStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C018D1680307A31C9ED7120 /* CGMStateModel.swift */; };
+		BD0B2EF32C5998E600B3298F /* MealPresetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD0B2EF22C5998E600B3298F /* MealPresetView.swift */; };
 		BD1661312B82ADAB00256551 /* CustomProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1661302B82ADAB00256551 /* CustomProgressView.swift */; };
 		BD2B464E0745FBE7B79913F4 /* NightscoutConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF768BD6264FF7D71D66767 /* NightscoutConfigProvider.swift */; };
 		BD2FF1A02AE29D43005D1C5D /* CheckboxToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */; };
@@ -309,6 +310,8 @@
 		BD7DA9A92AE06E9200601B20 /* BolusCalculatorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9A82AE06E9200601B20 /* BolusCalculatorStateModel.swift */; };
 		BD7DA9AC2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9AB2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift */; };
 		BDB3C1192C03DD1000CEEAA1 /* UserDefaultsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB3C1182C03DD1000CEEAA1 /* UserDefaultsExtension.swift */; };
+		BDB899882C564509006F3298 /* ForeCastChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB899872C564509006F3298 /* ForeCastChart.swift */; };
+		BDB8998A2C565D0C006F3298 /* CarbsGlucose+helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB899892C565D0B006F3298 /* CarbsGlucose+helper.swift */; };
 		BDBAACFA2C2D439700370AAE /* OverrideData.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBAACF92C2D439700370AAE /* OverrideData.swift */; };
 		BDC2EA452C3043B000E5BBD0 /* OverrideStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC2EA442C3043B000E5BBD0 /* OverrideStorage.swift */; };
 		BDC2EA472C3045AD00E5BBD0 /* Override.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC2EA462C3045AD00E5BBD0 /* Override.swift */; };
@@ -322,6 +325,7 @@
 		BDF530D82B40F8AC002CAF43 /* LockScreenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDF530D72B40F8AC002CAF43 /* LockScreenView.swift */; };
 		BDFD165A2AE40438007F0DDA /* BolusRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFD16592AE40438007F0DDA /* BolusRootView.swift */; };
 		BF1667ADE69E4B5B111CECAE /* ManualTempBasalProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680C4420C9A345D46D90D06C /* ManualTempBasalProvider.swift */; };
+		C20BC6CE2C66FBFD002BC1C6 /* Rounding.swift in Sources */ = {isa = PBXBuildFile; fileRef = C20BC6CD2C66FBFD002BC1C6 /* Rounding.swift */; };
 		C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFCFE0781F9074C2917890E8 /* ManualTempBasalStateModel.swift */; };
 		CA370FC152BC98B3D1832968 /* BasalProfileEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */; };
 		CC41E29A2B1E1F460070974F /* HistoryLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC41E2992B1E1F460070974F /* HistoryLayout.swift */; };
@@ -433,6 +437,7 @@
 		DDD1631A2C4C695E00CD525A /* EditOverrideForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163192C4C695E00CD525A /* EditOverrideForm.swift */; };
 		DDD1631C2C4C697400CD525A /* AddOverrideForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD1631B2C4C697400CD525A /* AddOverrideForm.swift */; };
 		DDD1631F2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DDD1631D2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodeld */; };
+		DDF847E62C5D66490049BB3B /* AddMealPresetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF847E52C5D66490049BB3B /* AddMealPresetView.swift */; };
 		E00EEC0327368630002FF094 /* ServiceAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = E00EEBFD27368630002FF094 /* ServiceAssembly.swift */; };
 		E00EEC0427368630002FF094 /* SecurityAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = E00EEBFE27368630002FF094 /* SecurityAssembly.swift */; };
 		E00EEC0527368630002FF094 /* StorageAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = E00EEBFF27368630002FF094 /* StorageAssembly.swift */; };
@@ -900,6 +905,7 @@
 		B9B5C0607505A38F256BF99A /* CGMDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CGMDataFlow.swift; sourceTree = "<group>"; };
 		B9CAAEFB2AE70836000F68BC /* branch.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = branch.txt; sourceTree = SOURCE_ROOT; };
 		BA49538D56989D8DA6FCF538 /* TargetsEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorDataFlow.swift; sourceTree = "<group>"; };
+		BD0B2EF22C5998E600B3298F /* MealPresetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealPresetView.swift; sourceTree = "<group>"; };
 		BD1661302B82ADAB00256551 /* CustomProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomProgressView.swift; sourceTree = "<group>"; };
 		BD1CF8B72C1A4A8400CB930A /* ConfigOverride.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ConfigOverride.xcconfig; sourceTree = "<group>"; };
 		BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxToggleStyle.swift; sourceTree = "<group>"; };
@@ -910,6 +916,8 @@
 		BD7DA9A82AE06E9200601B20 /* BolusCalculatorStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorStateModel.swift; sourceTree = "<group>"; };
 		BD7DA9AB2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorConfigRootView.swift; sourceTree = "<group>"; };
 		BDB3C1182C03DD1000CEEAA1 /* UserDefaultsExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsExtension.swift; sourceTree = "<group>"; };
+		BDB899872C564509006F3298 /* ForeCastChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForeCastChart.swift; sourceTree = "<group>"; };
+		BDB899892C565D0B006F3298 /* CarbsGlucose+helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CarbsGlucose+helper.swift"; sourceTree = "<group>"; };
 		BDBAACF92C2D439700370AAE /* OverrideData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideData.swift; sourceTree = "<group>"; };
 		BDC2EA442C3043B000E5BBD0 /* OverrideStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideStorage.swift; sourceTree = "<group>"; };
 		BDC2EA462C3045AD00E5BBD0 /* Override.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Override.swift; sourceTree = "<group>"; };
@@ -924,6 +932,7 @@
 		BDFD16592AE40438007F0DDA /* BolusRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusRootView.swift; sourceTree = "<group>"; };
 		BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorRootView.swift; sourceTree = "<group>"; };
 		C19984D62EFC0035A9E9644D /* BolusProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BolusProvider.swift; sourceTree = "<group>"; };
+		C20BC6CD2C66FBFD002BC1C6 /* Rounding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Rounding.swift; sourceTree = "<group>"; };
 		C377490C77661D75E8C50649 /* ManualTempBasalRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalRootView.swift; sourceTree = "<group>"; };
 		C8D1A7CA8C10C4403D4BBFA7 /* BolusDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BolusDataFlow.swift; sourceTree = "<group>"; };
 		CC41E2992B1E1F460070974F /* HistoryLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryLayout.swift; sourceTree = "<group>"; };
@@ -1036,6 +1045,7 @@
 		DDD163192C4C695E00CD525A /* EditOverrideForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditOverrideForm.swift; sourceTree = "<group>"; };
 		DDD1631B2C4C697400CD525A /* AddOverrideForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddOverrideForm.swift; sourceTree = "<group>"; };
 		DDD1631E2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = TrioCoreDataPersistentContainer.xcdatamodel; sourceTree = "<group>"; };
+		DDF847E52C5D66490049BB3B /* AddMealPresetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddMealPresetView.swift; sourceTree = "<group>"; };
 		E00EEBFD27368630002FF094 /* ServiceAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceAssembly.swift; sourceTree = "<group>"; };
 		E00EEBFE27368630002FF094 /* SecurityAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecurityAssembly.swift; sourceTree = "<group>"; };
 		E00EEBFF27368630002FF094 /* StorageAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StorageAssembly.swift; sourceTree = "<group>"; };
@@ -1838,6 +1848,7 @@
 				581516A32BCED84A00BF67D7 /* DebuggingIdentifiers.swift */,
 				DD1DB7CB2BECCA1F0048B367 /* BuildDetails.swift */,
 				11E79B122C5F2585007893C2 /* BGColor.swift */,
+				C20BC6CD2C66FBFD002BC1C6 /* Rounding.swift */,
 			);
 			path = Helpers;
 			sourceTree = "<group>";
@@ -2113,6 +2124,7 @@
 				582FAE422C05102C00D1C13F /* CoreDataError.swift */,
 				BDF34EBD2C0A31D000D51995 /* CustomNotification.swift */,
 				BDCD47AE2C1F3F1700F8BCD5 /* OverrideStored+helper.swift */,
+				BDB899892C565D0B006F3298 /* CarbsGlucose+helper.swift */,
 			);
 			path = Helper;
 			sourceTree = "<group>";
@@ -2235,6 +2247,9 @@
 			children = (
 				BDFD16592AE40438007F0DDA /* BolusRootView.swift */,
 				58237D9D2BCF0A6B00A47A79 /* PopupView.swift */,
+				BDB899872C564509006F3298 /* ForeCastChart.swift */,
+				BD0B2EF22C5998E600B3298F /* MealPresetView.swift */,
+				DDF847E52C5D66490049BB3B /* AddMealPresetView.swift */,
 			);
 			path = View;
 			sourceTree = "<group>";
@@ -3012,6 +3027,7 @@
 				DD57C4BD2C4C7103001A5B28 /* PumpEventStored+CoreDataProperties.swift in Sources */,
 				DD57C4BE2C4C7103001A5B28 /* TempBasalStored+CoreDataClass.swift in Sources */,
 				DD57C4BF2C4C7103001A5B28 /* TempBasalStored+CoreDataProperties.swift in Sources */,
+				BD0B2EF32C5998E600B3298F /* MealPresetView.swift in Sources */,
 				DD57C4C02C4C7103001A5B28 /* TempTargetsSlider+CoreDataClass.swift in Sources */,
 				DD57C4C12C4C7103001A5B28 /* TempTargetsSlider+CoreDataProperties.swift in Sources */,
 				DD57C4C22C4C7103001A5B28 /* Forecast+CoreDataClass.swift in Sources */,
@@ -3032,6 +3048,7 @@
 				DD57C4D12C4C7103001A5B28 /* ImportError+CoreDataProperties.swift in Sources */,
 				DD57C4D22C4C7103001A5B28 /* StatsData+CoreDataClass.swift in Sources */,
 				DD57C4D32C4C7103001A5B28 /* StatsData+CoreDataProperties.swift in Sources */,
+				DDF847E62C5D66490049BB3B /* AddMealPresetView.swift in Sources */,
 				3811DEAE25C9D88300A708ED /* Cache.swift in Sources */,
 				383420D625FFE38C002D46C1 /* LoopView.swift in Sources */,
 				3811DEAD25C9D88300A708ED /* UserDefaults+Cache.swift in Sources */,
@@ -3082,9 +3099,11 @@
 				6B1A8D2E2B156EEF00E76752 /* LiveActivityBridge.swift in Sources */,
 				581516A92BCEEDF800BF67D7 /* NSPredicates.swift in Sources */,
 				38DAB28A260D349500F74C1A /* FetchGlucoseManager.swift in Sources */,
+				BDB8998A2C565D0C006F3298 /* CarbsGlucose+helper.swift in Sources */,
 				38F37828261260DC009DB701 /* Color+Extensions.swift in Sources */,
 				BDCD47AF2C1F3F1700F8BCD5 /* OverrideStored+helper.swift in Sources */,
 				3811DE3F25C9D4A100A708ED /* SettingsStateModel.swift in Sources */,
+				C20BC6CE2C66FBFD002BC1C6 /* Rounding.swift in Sources */,
 				CE7CA3582A064E2F004BE681 /* ListStateView.swift in Sources */,
 				193F6CDD2A512C8F001240FD /* Loops.swift in Sources */,
 				38B4F3CB25E502E200E76A18 /* WeakObjectSet.swift in Sources */,
@@ -3112,6 +3131,7 @@
 				CE7CA3532A064973004BE681 /* tempPresetIntent.swift in Sources */,
 				D6DEC113821A7F1056C4AA1E /* NightscoutConfigDataFlow.swift in Sources */,
 				38E98A3025F52FF700C0CED0 /* Config.swift in Sources */,
+				BDB899882C564509006F3298 /* ForeCastChart.swift in Sources */,
 				110AEDE32C5193D200615CC9 /* BolusIntent.swift in Sources */,
 				DDD1631A2C4C695E00CD525A /* EditOverrideForm.swift in Sources */,
 				CE1856F72ADC4869007E39C7 /* CarbPresetIntentRequest.swift in Sources */,

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


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


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


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


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


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

@@ -44,6 +44,7 @@
   "yGridLines" : true,
   "oneDimensionalGraph" : false,
   "rulerMarks" : true,
+  "displayForecastsAsLines": false,
   "maxCarbs": 250,
   "maxFat": 250,
   "maxProtein": 250,

+ 56 - 20
FreeAPS/Sources/APS/APSManager.swift

@@ -24,6 +24,7 @@ protocol APSManager {
     func makeProfiles() async throws -> Bool
     func determineBasal() async -> Bool
     func determineBasalSync() async
+    func simulateDetermineBasal(carbs: Decimal, iob: Decimal) async -> Determination?
     func roundBolus(amount: Decimal) -> Decimal
     var lastError: CurrentValueSubject<Error?, Never> { get }
     func cancelBolus() async
@@ -323,9 +324,9 @@ final class BaseAPSManager: APSManager, Injectable {
         return nil
     }
 
-    func autosens() async throws -> Bool {
-        guard let autosens = await storage.retrieveAsync(OpenAPS.Settings.autosense, as: Autosens.self),
-              (autosens.timestamp ?? .distantPast).addingTimeInterval(30.minutes.timeInterval) > Date()
+    func autosense() async throws -> Bool {
+        guard let autosense = await storage.retrieveAsync(OpenAPS.Settings.autosense, as: Autosens.self),
+              (autosense.timestamp ?? .distantPast).addingTimeInterval(30.minutes.timeInterval) > Date()
         else {
             let result = try await openAPS.autosense()
             return result != nil
@@ -372,11 +373,17 @@ final class BaseAPSManager: APSManager, Injectable {
 
         do {
             let now = Date()
-            let temp = await fetchCurrentTempBasal(date: now)
-            _ = try await makeProfiles()
-            _ = try await autosens()
-            _ = try await dailyAutotune()
-            let determination = try await openAPS.determineBasal(currentTemp: temp, clock: now)
+
+            // Start fetching asynchronously
+            let (currentTemp, profiles, autosense, dailyAutotune) = try await (
+                fetchCurrentTempBasal(date: now),
+                makeProfiles(),
+                autosense(),
+                dailyAutotune()
+            )
+
+            // Determine basal using the fetched temp and current time
+            let determination = try await openAPS.determineBasal(currentTemp: currentTemp, clock: now)
 
             if let determination = determination {
                 DispatchQueue.main.async {
@@ -398,6 +405,18 @@ final class BaseAPSManager: APSManager, Injectable {
         _ = await determineBasal()
     }
 
+    func simulateDetermineBasal(carbs: Decimal, iob: Decimal) async -> Determination? {
+        do {
+            let temp = await fetchCurrentTempBasal(date: Date.now)
+            return try await openAPS.determineBasal(currentTemp: temp, clock: Date(), carbs: carbs, iob: iob, simulation: true)
+        } catch {
+            debugPrint(
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Error occurred in invokeDummyDetermineBasalSync: \(error)"
+            )
+            return nil
+        }
+    }
+
     func makeProfiles() async throws -> Bool {
         let tunedProfile = await openAPS.makeProfiles(useAutotune: settings.useAutotune)
         if let basalProfile = tunedProfile?.basalProfile {
@@ -420,6 +439,10 @@ final class BaseAPSManager: APSManager, Injectable {
     private var bolusReporter: DoseProgressReporter?
 
     func enactBolus(amount: Double, isSMB: Bool) async {
+        if amount <= 0 {
+            return
+        }
+
         if let error = verifyStatus() {
             processError(error)
             processQueue.async {
@@ -624,7 +647,8 @@ final class BaseAPSManager: APSManager, Injectable {
         )
 
         let fetchedTempBasal = await privateContext.perform {
-            guard let tempBasalEvent = results.first,
+            guard let fetchedResults = results as? [PumpEventStored],
+                  let tempBasalEvent = fetchedResults.first,
                   let tempBasal = tempBasalEvent.tempBasal,
                   let eventTimestamp = tempBasalEvent.timestamp
             else {
@@ -896,7 +920,7 @@ final class BaseAPSManager: APSManager, Injectable {
 
     // fetch glucose for time interval
     func fetchGlucose(predicate: NSPredicate, fetchLimit: Int? = nil, batchSize: Int? = nil) async -> [GlucoseStored] {
-        await CoreDataStack.shared.fetchEntitiesAsync(
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             onContext: privateContext,
             predicate: predicate,
@@ -905,6 +929,12 @@ final class BaseAPSManager: APSManager, Injectable {
             fetchLimit: fetchLimit,
             batchSize: batchSize
         )
+
+        guard let glucoseResults = results as? [GlucoseStored] else {
+            return []
+        }
+
+        return glucoseResults
     }
 
     // TODO: - Refactor this whole shit here...
@@ -1142,28 +1172,34 @@ final class BaseAPSManager: APSManager, Injectable {
     }
 
     private func tddForStats() async -> (currentTDD: Decimal, tddTotalAverage: Decimal) {
-        let requestTDD = OrefDetermination.fetchRequest() as NSFetchRequest<OrefDetermination>
+        let requestTDD = OrefDetermination.fetchRequest() as NSFetchRequest<NSFetchRequestResult>
         let sort = NSSortDescriptor(key: "timestamp", ascending: false)
         let daysOf14Ago = Date().addingTimeInterval(-14.days.timeInterval)
         requestTDD.predicate = NSPredicate(format: "timestamp > %@", daysOf14Ago as NSDate)
         requestTDD.sortDescriptors = [sort]
         requestTDD.propertiesToFetch = ["timestamp", "totalDailyDose"]
+        requestTDD.resultType = .dictionaryResultType
 
-        var tdds = [OrefDetermination]()
         var currentTDD: Decimal = 0
         var tddTotalAverage: Decimal = 0
 
-        await privateContext.perform {
+        let results = await privateContext.perform {
             do {
-                try tdds = self.privateContext.fetch(requestTDD)
-
-                if !tdds.isEmpty {
-                    currentTDD = tdds[0].totalDailyDose?.decimalValue ?? 0
-                    let tddArray = tdds.compactMap({ insulin in insulin.totalDailyDose as? Decimal ?? 0 })
-                    tddTotalAverage = tddArray.reduce(0, +) / Decimal(tddArray.count)
-                }
+                let fetchedResults = try self.privateContext.fetch(requestTDD) as? [[String: Any]]
+                return fetchedResults ?? []
             } catch {
                 debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to get TDD Data for Statistics Upload")
+                return []
+            }
+        }
+
+        if !results.isEmpty {
+            if let latestTDD = results.first?["totalDailyDose"] as? NSDecimalNumber {
+                currentTDD = latestTDD.decimalValue
+            }
+            let tddArray = results.compactMap { ($0["totalDailyDose"] as? NSDecimalNumber)?.decimalValue }
+            if !tddArray.isEmpty {
+                tddTotalAverage = tddArray.reduce(0, +) / Decimal(tddArray.count)
             }
         }
 

+ 25 - 6
FreeAPS/Sources/APS/CGM/PluginSource.swift

@@ -135,13 +135,24 @@ extension PluginSource: CGMManagerDelegate {
         return glucoseStorage.lastGlucoseDate()
     }
 
-    func cgmManagerDidUpdateState(_: CGMManager) {
+    func cgmManagerDidUpdateState(_ cgmManager: CGMManager) {
         dispatchPrecondition(condition: .onQueue(processQueue))
-//        guard let g6Manager = manager as? TransmitterManager else {
-//            return
-//        }
-//        glucoseManager?.settingsManager.settings.uploadGlucose = g6Manager.shouldSyncToRemoteService
-//        UserDefaults.standard.dexcomTransmitterID = g6Manager.rawState["transmitterID"] as? String
+
+        guard let fetchGlucoseManager = glucoseManager else {
+            debug(
+                .deviceManager,
+                "Could not gracefully unwrap FetchGlucoseManager upon observing LoopKit's cgmManagerDidUpdateState"
+            )
+            return
+        }
+        // Adjust app-specific NS Upload setting value when CGM setting is changed
+        fetchGlucoseManager.settingsManager.settings.uploadGlucose = cgmManager.shouldSyncToRemoteService
+
+        fetchGlucoseManager.updateGlucoseSource(
+            cgmGlucoseSourceType: fetchGlucoseManager.settingsManager.settings.cgm,
+            cgmGlucosePluginId: fetchGlucoseManager.settingsManager.settings.cgmPluginIdentifier,
+            newManager: cgmManager as? CGMManagerUI
+        )
     }
 
     func credentialStoragePrefix(for _: CGMManager) -> String {
@@ -160,6 +171,14 @@ extension PluginSource: CGMManagerDelegate {
 
     private func readCGMResult(readingResult: CGMReadingResult) -> Result<[BloodGlucose], Error> {
         debug(.deviceManager, "PLUGIN CGM - Process CGM Reading Result launched with \(readingResult)")
+
+        if glucoseManager?.glucoseSource == nil {
+            debug(
+                .deviceManager,
+                "No glucose source available."
+            )
+        }
+
         switch readingResult {
         case let .newData(values):
 

+ 0 - 1
FreeAPS/Sources/APS/DeviceDataManager.swift

@@ -475,7 +475,6 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
         completion: @escaping (_ error: Error?) -> Void
     ) {
         dispatchPrecondition(condition: .onQueue(processQueue))
-        debug(.deviceManager, "New pump events:\n\(events.map(\.title).joined(separator: "\n"))")
 
         // filter buggy TBRs > maxBasal from MDT
         let events = events.filter {

+ 18 - 16
FreeAPS/Sources/APS/FetchGlucoseManager.swift

@@ -105,6 +105,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         if self.cgmGlucoseSourceType != cgmGlucoseSourceType || self.cgmGlucosePluginId != cgmGlucosePluginId {
             removeCalibrations()
             cgmManager = nil
+            glucoseSource = nil
         }
 
         self.cgmGlucoseSourceType = cgmGlucoseSourceType
@@ -125,23 +126,24 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
             saveConfigManager()
         }
 
-        switch self.cgmGlucoseSourceType {
-        case .none:
-            glucoseSource = nil
-        case .xdrip:
-            glucoseSource = AppGroupSource(from: "xDrip", cgmType: .xdrip)
-        case .nightscout:
-            glucoseSource = nightscoutManager
-        case .simulator:
-            glucoseSource = simulatorSource
-        case .glucoseDirect:
-            glucoseSource = AppGroupSource(from: "GlucoseDirect", cgmType: .glucoseDirect)
-        case .enlite:
-            glucoseSource = deviceDataManager
-        case .plugin:
-            glucoseSource = PluginSource(glucoseStorage: glucoseStorage, glucoseManager: self)
+        if glucoseSource == nil {
+            switch self.cgmGlucoseSourceType {
+            case .none:
+                glucoseSource = nil
+            case .xdrip:
+                glucoseSource = AppGroupSource(from: "xDrip", cgmType: .xdrip)
+            case .nightscout:
+                glucoseSource = nightscoutManager
+            case .simulator:
+                glucoseSource = simulatorSource
+            case .glucoseDirect:
+                glucoseSource = AppGroupSource(from: "GlucoseDirect", cgmType: .glucoseDirect)
+            case .enlite:
+                glucoseSource = deviceDataManager
+            case .plugin:
+                glucoseSource = PluginSource(glucoseStorage: glucoseStorage, glucoseManager: self)
+            }
         }
-        // update the config
     }
 
     /// Upload cgmManager from raw value

+ 145 - 52
FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift

@@ -93,7 +93,7 @@ final class OpenAPS {
     }
 
     func checkForCobIobUpdate(_ determination: Determination) async {
-        let previousDeterminations = await CoreDataStack.shared.fetchEntitiesAsync(
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OrefDetermination.self,
             onContext: context,
             predicate: NSPredicate.predicateFor30MinAgoForDetermination,
@@ -102,6 +102,10 @@ final class OpenAPS {
             fetchLimit: 2
         )
 
+        guard let previousDeterminations = results as? [OrefDetermination] else {
+            return
+        }
+
         // We need to get the second last Determination for this comparison because we have saved the current Determination already to Core Data
         if let previousDetermination = previousDeterminations.dropFirst().first {
             let iobChanged = previousDetermination.iob != decimalToNSDecimalNumber(determination.iob)
@@ -129,20 +133,24 @@ final class OpenAPS {
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             onContext: context,
-            predicate: NSPredicate.predicateForSixHoursAgo,
+            predicate: NSPredicate.predicateForOneDayAgoInMinutes,
             key: "date",
             ascending: false,
             fetchLimit: 72,
             batchSize: 24
         )
 
+        guard let glucoseResults = results as? [GlucoseStored] else {
+            return ""
+        }
+
         return await context.perform {
-            // convert to json
-            return self.jsonConverter.convertToJSON(results)
+            // convert to JSON
+            return self.jsonConverter.convertToJSON(glucoseResults)
         }
     }
 
-    private func fetchAndProcessCarbs() async -> String {
+    private func fetchAndProcessCarbs(additionalCarbs: Decimal? = nil) async -> String {
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
             onContext: context,
@@ -151,49 +159,78 @@ final class OpenAPS {
             ascending: false
         )
 
-        // convert to json
-        return await context.perform {
-            return self.jsonConverter.convertToJSON(results)
+        guard let carbResults = results as? [CarbEntryStored] else {
+            return ""
+        }
+
+        let json = await context.perform {
+            var jsonArray = self.jsonConverter.convertToJSON(carbResults)
+
+            if let additionalCarbs = additionalCarbs {
+                let additionalEntry = [
+                    "carbs": Double(additionalCarbs),
+                    "actualDate": ISO8601DateFormatter().string(from: Date()),
+                    "id": UUID().uuidString,
+                    "note": NSNull(),
+                    "protein": 0,
+                    "created_at": ISO8601DateFormatter().string(from: Date()),
+                    "isFPU": false,
+                    "fat": 0,
+                    "enteredBy": "Trio"
+                ] as [String: Any]
+
+                // Assuming jsonArray is a String, convert it to a list of dictionaries first
+                if let jsonData = jsonArray.data(using: .utf8) {
+                    var jsonList = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [[String: Any]]
+                    jsonList?.append(additionalEntry)
+
+                    // Convert back to JSON string
+                    if let updatedJsonData = try? JSONSerialization
+                        .data(withJSONObject: jsonList ?? [], options: .prettyPrinted)
+                    {
+                        jsonArray = String(data: updatedJsonData, encoding: .utf8) ?? jsonArray
+                    }
+                }
+            }
+
+            return jsonArray
         }
+
+        return json
     }
 
     private func fetchPumpHistoryObjectIDs() async -> [NSManagedObjectID]? {
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: PumpEventStored.self,
             onContext: context,
-            predicate: NSPredicate.pumpHistoryLast24h,
+            predicate: NSPredicate.pumpHistoryLast1440Minutes,
             key: "timestamp",
             ascending: false,
             batchSize: 50
         )
+
+        guard let pumpEventResults = results as? [PumpEventStored] else {
+            return nil
+        }
+
         return await context.perform {
-            return results.map(\.objectID)
+            return pumpEventResults.map(\.objectID)
         }
     }
 
-    private func parsePumpHistory(_ pumpHistoryObjectIDs: [NSManagedObjectID]) async -> String {
+    private func parsePumpHistory(_ pumpHistoryObjectIDs: [NSManagedObjectID], iob: Decimal? = nil) async -> String {
         // Return an empty JSON object if the list of object IDs is empty
         guard !pumpHistoryObjectIDs.isEmpty else { return "{}" }
 
         // Execute all operations on the background context
         return await context.perform {
-            // Load the pump events from the object IDs
-            let pumpHistory: [PumpEventStored] = pumpHistoryObjectIDs
-                .compactMap { self.context.object(with: $0) as? PumpEventStored }
-
-            // Create the DTOs
-            let dtos: [PumpEventDTO] = pumpHistory.flatMap { event -> [PumpEventDTO] in
-                var eventDTOs: [PumpEventDTO] = []
-                if let bolusDTO = event.toBolusDTOEnum() {
-                    eventDTOs.append(bolusDTO)
-                }
-                if let tempBasalDTO = event.toTempBasalDTOEnum() {
-                    eventDTOs.append(tempBasalDTO)
-                }
-                if let tempBasalDurationDTO = event.toTempBasalDurationDTOEnum() {
-                    eventDTOs.append(tempBasalDurationDTO)
-                }
-                return eventDTOs
+            // Load and map pump events to DTOs
+            var dtos = self.loadAndMapPumpEvents(pumpHistoryObjectIDs)
+
+            // Optionally add the IOB as a DTO
+            if let iob = iob {
+                let iobDTO = self.createIOBDTO(iob: iob)
+                dtos.insert(iobDTO, at: 0)
             }
 
             // Convert the DTOs to JSON
@@ -201,19 +238,64 @@ final class OpenAPS {
         }
     }
 
-    func determineBasal(currentTemp: TempBasal, clock: Date = Date()) async throws -> Determination? {
-        debug(.openAPS, "Start determineBasal")
+    private func loadAndMapPumpEvents(_ pumpHistoryObjectIDs: [NSManagedObjectID]) -> [PumpEventDTO] {
+        // Load the pump events from the object IDs
+        let pumpHistory: [PumpEventStored] = pumpHistoryObjectIDs
+            .compactMap { self.context.object(with: $0) as? PumpEventStored }
+
+        // Create the DTOs
+        let dtos: [PumpEventDTO] = pumpHistory.flatMap { event -> [PumpEventDTO] in
+            var eventDTOs: [PumpEventDTO] = []
+            if let bolusDTO = event.toBolusDTOEnum() {
+                eventDTOs.append(bolusDTO)
+            }
+            if let tempBasalDTO = event.toTempBasalDTOEnum() {
+                eventDTOs.append(tempBasalDTO)
+            }
+            if let tempBasalDurationDTO = event.toTempBasalDurationDTOEnum() {
+                eventDTOs.append(tempBasalDurationDTO)
+            }
+            return eventDTOs
+        }
+        return dtos
+    }
+
+    private func createIOBDTO(iob: Decimal) -> PumpEventDTO {
+        let oneSecondAgo = Calendar.current
+            .date(
+                byAdding: .second,
+                value: -1,
+                to: Date()
+            )! // adding -1s to the current Date ensures that oref actually uses the mock entry to calculate iob and not guard it away
+        let dateFormatted = PumpEventStored.dateFormatter.string(from: oneSecondAgo)
+
+        let bolusDTO = BolusDTO(
+            id: UUID().uuidString,
+            timestamp: dateFormatted,
+            amount: Double(iob),
+            isExternal: false,
+            isSMB: true,
+            duration: 0,
+            _type: "Bolus"
+        )
+        return .bolus(bolusDTO)
+    }
 
-        // clock
-        let dateFormatted = OpenAPS.dateFormatter.string(from: clock)
-        let dateFormattedAsString = "\"\(dateFormatted)\""
+    func determineBasal(
+        currentTemp: TempBasal,
+        clock: Date = Date(),
+        carbs: Decimal? = nil,
+        iob: Decimal? = nil,
+        simulation: Bool = false
+    ) async throws -> Determination? {
+        debug(.openAPS, "Start determineBasal")
 
         // temp_basal
         let tempBasal = currentTemp.rawJSON
 
         // Perform asynchronous calls in parallel
         async let pumpHistoryObjectIDs = fetchPumpHistoryObjectIDs() ?? []
-        async let carbs = fetchAndProcessCarbs()
+        async let carbs = fetchAndProcessCarbs(additionalCarbs: carbs ?? 0)
         async let glucose = fetchAndProcessGlucose()
         async let oref2 = oref2()
         async let profileAsync = loadFileFromStorageAsync(name: Settings.profile)
@@ -234,7 +316,7 @@ final class OpenAPS {
             reservoir,
             preferences
         ) = await (
-            parsePumpHistory(await pumpHistoryObjectIDs),
+            parsePumpHistory(await pumpHistoryObjectIDs, iob: iob),
             carbs,
             glucose,
             oref2,
@@ -245,28 +327,28 @@ final class OpenAPS {
             preferencesAsync
         )
 
-        // TODO: - Save and fetch profile/basalProfile in/from UserDefaults!
-
-        // Meal
+        // Meal calculation
         let meal = try await self.meal(
             pumphistory: pumpHistoryJSON,
             profile: profile,
             basalProfile: basalProfile,
-            clock: dateFormattedAsString,
+            clock: clock,
             carbs: carbsAsJSON,
             glucose: glucoseAsJSON
         )
 
-        // IOB
+        // IOB calculation
         let iob = try await self.iob(
             pumphistory: pumpHistoryJSON,
             profile: profile,
-            clock: dateFormattedAsString,
+            clock: clock,
             autosens: autosens.isEmpty ? .null : autosens
         )
 
         // TODO: refactor this to core data
-        storage.save(iob, as: Monitor.iob)
+        if !simulation {
+            storage.save(iob, as: Monitor.iob)
+        }
 
         // Determine basal
         let orefDetermination = try await determineBasal(
@@ -291,8 +373,10 @@ final class OpenAPS {
             // AAPS does it the same way! we'll follow their example!
             determination.timestamp = deliverAt
 
-            // save to core data asynchronously
-            await processDetermination(determination)
+            if !simulation {
+                // save to core data asynchronously
+                await processDetermination(determination)
+            }
 
             return determination
         } else {
@@ -311,13 +395,21 @@ final class OpenAPS {
             let tenDaysAgo = Date().addingTimeInterval(-10.days.timeInterval)
             let twoHoursAgo = Date().addingTimeInterval(-2.hours.timeInterval)
 
-            var uniqueEvents = [OrefDetermination]()
-            let requestTDD = OrefDetermination.fetchRequest() as NSFetchRequest<OrefDetermination>
+            var uniqueEvents = [[String: Any]]()
+            let requestTDD = OrefDetermination.fetchRequest() as NSFetchRequest<NSFetchRequestResult>
             requestTDD.predicate = NSPredicate(format: "timestamp > %@ AND totalDailyDose > 0", tenDaysAgo as NSDate)
             requestTDD.propertiesToFetch = ["timestamp", "totalDailyDose"]
             let sortTDD = NSSortDescriptor(key: "timestamp", ascending: true)
             requestTDD.sortDescriptors = [sortTDD]
-            try? uniqueEvents = self.context.fetch(requestTDD)
+            requestTDD.resultType = .dictionaryResultType
+
+            do {
+                if let fetchedResults = try self.context.fetch(requestTDD) as? [[String: Any]] {
+                    uniqueEvents = fetchedResults
+                }
+            } catch {
+                debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch TDD Data")
+            }
 
             var sliderArray = [TempTargetsSlider]()
             let requestIsEnbled = TempTargetsSlider.fetchRequest() as NSFetchRequest<TempTargetsSlider>
@@ -342,12 +434,13 @@ final class OpenAPS {
             requestTempTargets.fetchLimit = 1
             try? tempTargetsArray = self.context.fetch(requestTempTargets)
 
-            let total = uniqueEvents.compactMap({ each in each.totalDailyDose as? Decimal ?? 0 }).reduce(0, +)
+            let total = uniqueEvents.compactMap({ ($0["totalDailyDose"] as? NSDecimalNumber)?.decimalValue ?? 0 }).reduce(0, +)
             var indeces = uniqueEvents.count
             // Only fetch once. Use same (previous) fetch
-            let twoHoursArray = uniqueEvents.filter({ ($0.timestamp ?? Date()) >= twoHoursAgo })
+            let twoHoursArray = uniqueEvents.filter({ ($0["timestamp"] as? Date ?? Date()) >= twoHoursAgo })
             var nrOfIndeces = twoHoursArray.count
-            let totalAmount = twoHoursArray.compactMap({ each in each.totalDailyDose as? Decimal ?? 0 }).reduce(0, +)
+            let totalAmount = twoHoursArray.compactMap({ ($0["totalDailyDose"] as? NSDecimalNumber)?.decimalValue ?? 0 })
+                .reduce(0, +)
 
             var temptargetActive = tempTargetsArray.first?.active ?? false
             let isPercentageEnabled = sliderArray.first?.enabled ?? false
@@ -357,7 +450,7 @@ final class OpenAPS {
             var unlimited = overrideArray.first?.indefinite ?? true
             var disableSMBs = overrideArray.first?.smbIsOff ?? false
 
-            let currentTDD = (uniqueEvents.last?.totalDailyDose ?? 0) as Decimal
+            let currentTDD = (uniqueEvents.last?["totalDailyDose"] as? NSDecimalNumber)?.decimalValue ?? 0
 
             if indeces == 0 {
                 indeces = 1
@@ -519,7 +612,7 @@ final class OpenAPS {
         debug(.openAPS, "AUTOSENS: \(autosenseResult)")
         if var autosens = Autosens(from: autosenseResult) {
             autosens.timestamp = Date()
-            storage.save(autosens, as: Settings.autosense)
+            await storage.saveAsync(autosens, as: Settings.autosense)
 
             return autosens
         } else {

+ 10 - 2
FreeAPS/Sources/APS/Storage/CarbsStorage.swift

@@ -156,6 +156,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             newItem.carbs = Double(truncating: NSDecimalNumber(decimal: entry.carbs))
             newItem.fat = Double(truncating: NSDecimalNumber(decimal: entry.fat ?? 0))
             newItem.protein = Double(truncating: NSDecimalNumber(decimal: entry.protein ?? 0))
+            newItem.note = entry.note
             newItem.id = UUID()
             newItem.isFPU = false
             newItem.isUploadedToNS = false
@@ -247,8 +248,12 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             ascending: false
         )
 
+        guard let carbEntries = results as? [CarbEntryStored] else {
+            return []
+        }
+
         return await coredataContext.perform {
-            return results.map { result in
+            return carbEntries.map { result in
                 NightscoutTreatment(
                     duration: nil,
                     rawDuration: nil,
@@ -260,6 +265,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
                     enteredBy: CarbsEntry.manual,
                     bolus: nil,
                     insulin: nil,
+                    notes: result.note,
                     carbs: Decimal(result.carbs),
                     fat: Decimal(result.fat),
                     protein: Decimal(result.protein),
@@ -281,8 +287,10 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             ascending: false
         )
 
+        guard let fpuEntries = results as? [CarbEntryStored] else { return [] }
+
         return await coredataContext.perform {
-            return results.map { result in
+            return fpuEntries.map { result in
                 NightscoutTreatment(
                     duration: nil,
                     rawDuration: nil,

+ 22 - 22
FreeAPS/Sources/APS/Storage/DeterminationStorage.swift

@@ -26,8 +26,11 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
             ascending: false,
             fetchLimit: 1
         )
+
+        guard let fetchedResults = results as? [OrefDetermination] else { return [] }
+
         return await backgroundContext.perform {
-            results.map(\.objectID)
+            fetchedResults.map(\.objectID)
         }
     }
 
@@ -75,38 +78,41 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
     }
 
     // Convert NSSet to array of Ints for Predictions
-    func parseForecastValues(ofType _: String, from determinationID: NSManagedObjectID) async -> [Int]? {
+    func parseForecastValues(ofType type: String, from determinationID: NSManagedObjectID) async -> [Int]? {
         let forecastIDs = await getForecastIDs(for: determinationID, in: backgroundContext)
 
         var forecastValuesList: [Int] = []
 
         for forecastID in forecastIDs {
-            let forecastValueIDs = await getForecastValueIDs(for: forecastID, in: backgroundContext)
-
             await backgroundContext.perform {
-                for forecastValueID in forecastValueIDs {
-                    if let forecastValue = try? self.backgroundContext.existingObject(with: forecastValueID) as? ForecastValue {
-                        let forecastValueInt = Int(forecastValue.value)
-                        forecastValuesList.append(forecastValueInt)
+                if let forecast = try? self.backgroundContext.existingObject(with: forecastID) as? Forecast {
+                    // Filter the forecast based on the type
+                    if forecast.type == type {
+                        let forecastValueIDs = forecast.forecastValues?.sorted(by: { $0.index < $1.index }).map(\.objectID) ?? []
+
+                        for forecastValueID in forecastValueIDs {
+                            if let forecastValue = try? self.backgroundContext
+                                .existingObject(with: forecastValueID) as? ForecastValue
+                            {
+                                let forecastValueInt = Int(forecastValue.value)
+                                forecastValuesList.append(forecastValueInt)
+                            }
+                        }
                     }
                 }
             }
         }
 
-        return forecastValuesList
+        return forecastValuesList.isEmpty ? nil : forecastValuesList
     }
 
     func getOrefDeterminationNotYetUploadedToNightscout(_ determinationIds: [NSManagedObjectID]) async -> Determination? {
         var result: Determination?
 
         guard let determinationId = determinationIds.first else {
-            print("No determination ID found.")
             return nil
         }
 
-        print("Using context: \(backgroundContext)")
-        print("Determination ID: \(determinationId)")
-
         let predictions = Predictions(
             iob: await parseForecastValues(ofType: "iob", from: determinationId),
             zt: await parseForecastValues(ofType: "zt", from: determinationId),
@@ -118,15 +124,9 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
             do {
                 let orefDetermination = try self.backgroundContext.existingObject(with: determinationId) as? OrefDetermination
 
-                // Log the type of the fetched object
-                print("Fetched object type: \(type(of: orefDetermination))")
-                print("Fetched object description: \(orefDetermination)")
-
                 // Check if the fetched object is of the expected type
                 if let orefDetermination = orefDetermination {
-                    print("Successfully cast to OrefDetermination")
                     let forecastSet = orefDetermination.forecasts
-                    print("Fetched forecast set: \(forecastSet)")
 
                     result = Determination(
                         id: orefDetermination.id ?? UUID(),
@@ -160,11 +160,11 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
                         carbRatio: self.decimal(from: orefDetermination.carbRatio),
                         received: orefDetermination.enacted // this is actually part of NS...
                     )
-                } else {
-                    print("Fetched object is not of type OrefDetermination")
                 }
             } catch {
-                print("Failed to fetch managed object: \(error)")
+                debugPrint(
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch managed object with error: \(error.localizedDescription)"
+                )
             }
 
             return result

+ 15 - 7
FreeAPS/Sources/APS/Storage/GlucoseStorage.swift

@@ -7,6 +7,7 @@ import Swinject
 
 protocol GlucoseStorage {
     func storeGlucose(_ glucose: [BloodGlucose])
+    func isGlucoseDataFresh(_ glucoseDate: Date?) -> Bool
     func syncDate() -> Date
     func filterTooFrequentGlucose(_ glucose: [BloodGlucose], at: Date) -> [BloodGlucose]
     func lastGlucoseDate() -> Date
@@ -46,8 +47,6 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
     func storeGlucose(_ glucose: [BloodGlucose]) {
         processQueue.sync {
-            debug(.deviceManager, "Start storage of glucose data")
-
             self.coredataContext.perform {
                 let datesToCheck: Set<Date?> = Set(glucose.compactMap { $0.dateString as Date? })
                 let fetchRequest: NSFetchRequest<NSFetchRequestResult> = GlucoseStored.fetchRequest()
@@ -81,8 +80,6 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                         glucoseEntry.date = entry.dateString
                         glucoseEntry.direction = entry.direction?.rawValue
                         glucoseEntry.isUploadedToNS = false /// the value is not uploaded to NS (yet)
-                        debugPrint("\(DebuggingIdentifiers.failed)")
-                        debugPrint("\(String(describing: glucoseEntry.direction))")
                         return false // Continue processing
                     }
                 )
@@ -90,7 +87,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                 // process batch insert
                 do {
                     try self.coredataContext.execute(batchInsert)
-                    debugPrint("Glucose Storage: \(#function) \(DebuggingIdentifiers.succeeded) saved glucose to Core Data")
+//                    debugPrint("Glucose Storage: \(#function) \(DebuggingIdentifiers.succeeded) saved glucose to Core Data")
 
                     // Send notification for triggering a fetch in Home State Model to update the Glucose Array
                     /// This is necessary because changes only get merged automatically into the viewContext because of the Persistent History Tracking
@@ -162,6 +159,11 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
         }
     }
 
+    func isGlucoseDataFresh(_ glucoseDate: Date?) -> Bool {
+        guard let glucoseDate = glucoseDate else { return false }
+        return glucoseDate > Date().addingTimeInterval(-6 * 60)
+    }
+
     func syncDate() -> Date {
         let fr = GlucoseStored.fetchRequest()
         fr.predicate = NSPredicate.predicateForOneDayAgo
@@ -243,8 +245,11 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             ascending: false,
             fetchLimit: 288
         )
+
+        guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+
         return await coredataContext.perform {
-            return results.map { result in
+            return fetchedResults.map { result in
                 BloodGlucose(
                     _id: result.id?.uuidString ?? UUID().uuidString,
                     sgv: Int(result.glucose),
@@ -271,8 +276,11 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             ascending: false,
             fetchLimit: 288
         )
+
+        guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+
         return await coredataContext.perform {
-            return results.map { result in
+            return fetchedResults.map { result in
                 NightscoutTreatment(
                     duration: nil,
                     rawDuration: nil,

+ 16 - 6
FreeAPS/Sources/APS/Storage/OverrideStorage.swift

@@ -45,8 +45,10 @@ final class BaseOverrideStorage: OverrideStorage, Injectable {
             fetchLimit: 1
         )
 
+        guard let fetchedResults = results as? [OverrideStored] else { return [] }
+
         return await backgroundContext.perform {
-            return results.map(\.objectID)
+            return fetchedResults.map(\.objectID)
         }
     }
 
@@ -60,14 +62,16 @@ final class BaseOverrideStorage: OverrideStorage, Injectable {
             fetchLimit: fetchLimit
         )
 
+        guard let fetchedResults = results as? [OverrideStored] else { return [] }
+
         return await backgroundContext.perform {
-            return results.map(\.objectID)
+            return fetchedResults.map(\.objectID)
         }
     }
 
     /// Returns the NSManagedObjectID of the Override Presets
     func fetchForOverridePresets() async -> [NSManagedObjectID] {
-        let result = await CoreDataStack.shared.fetchEntitiesAsync(
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,
             onContext: backgroundContext,
             predicate: NSPredicate.allOverridePresets,
@@ -75,8 +79,10 @@ final class BaseOverrideStorage: OverrideStorage, Injectable {
             ascending: true
         )
 
+        guard let fetchedResults = results as? [OverrideStored] else { return [] }
+
         return await backgroundContext.perform {
-            return result.map(\.objectID)
+            return fetchedResults.map(\.objectID)
         }
     }
 
@@ -206,7 +212,7 @@ final class BaseOverrideStorage: OverrideStorage, Injectable {
     }
 
     func getOverridesNotYetUploadedToNightscout() async -> [NightscoutExercise] {
-        let fetchedOverrides = await CoreDataStack.shared.fetchEntitiesAsync(
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,
             onContext: backgroundContext,
             predicate: NSPredicate.lastActiveOverrideNotYetUploadedToNightscout,
@@ -214,6 +220,8 @@ final class BaseOverrideStorage: OverrideStorage, Injectable {
             ascending: false
         )
 
+        guard let fetchedOverrides = results as? [OverrideStored] else { return [] }
+
         return await backgroundContext.perform {
             return fetchedOverrides.map { override in
                 let duration = override.indefinite ? 1440 : override.duration ?? 0 // 1440 min = 1 day
@@ -230,7 +238,7 @@ final class BaseOverrideStorage: OverrideStorage, Injectable {
     }
 
     func getOverrideRunsNotYetUploadedToNightscout() async -> [NightscoutExercise] {
-        let fetchedOverrideRuns = await CoreDataStack.shared.fetchEntitiesAsync(
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideRunStored.self,
             onContext: backgroundContext,
             predicate: NSPredicate(
@@ -242,6 +250,8 @@ final class BaseOverrideStorage: OverrideStorage, Injectable {
             ascending: false
         )
 
+        guard let fetchedOverrideRuns = results as? [OverrideRunStored] else { return [] }
+
         return await backgroundContext.perform {
             return fetchedOverrideRuns.map { overrideRun in
                 var durationInMinutes = (overrideRun.endDate?.timeIntervalSince(overrideRun.startDate ?? Date()) ?? 1) / 60

+ 3 - 1
FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift

@@ -253,7 +253,7 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
     }
 
     func getPumpHistoryNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
-        let fetchedPumpEvents = await CoreDataStack.shared.fetchEntitiesAsync(
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: PumpEventStored.self,
             onContext: context,
             predicate: NSPredicate.pumpEventsNotYetUploadedToNightscout,
@@ -262,6 +262,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
             fetchLimit: 288
         )
 
+        guard let fetchedPumpEvents = results as? [PumpEventStored] else { return [] }
+
         return await context.perform { [self] in
             fetchedPumpEvents.map { event in
                 switch event.type {

+ 8 - 0
FreeAPS/Sources/Helpers/Rounding.swift

@@ -0,0 +1,8 @@
+import Foundation
+
+func rounded(_ value: Decimal, scale: Int, roundingMode: NSDecimalNumber.RoundingMode) -> Decimal {
+    var result = Decimal()
+    var toRound = value
+    NSDecimalRound(&result, &toRound, scale, roundingMode)
+    return result
+}

+ 28 - 5
FreeAPS/Sources/Models/BloodGlucose.swift

@@ -107,28 +107,51 @@ enum GlucoseUnits: String, JSON, Equatable {
 
 extension Int {
     var asMmolL: Decimal {
-        Decimal(self) * GlucoseUnits.exchangeRate
+        FreeAPS.rounded(Decimal(self) * GlucoseUnits.exchangeRate, scale: 1, roundingMode: .plain)
+    }
+
+    var formattedAsMmolL: String {
+        NumberFormatter.glucoseFormatter.string(from: asMmolL as NSDecimalNumber) ?? "\(asMmolL)"
     }
 }
 
 extension Decimal {
     var asMmolL: Decimal {
-        self * GlucoseUnits.exchangeRate
+        FreeAPS.rounded(self * GlucoseUnits.exchangeRate, scale: 1, roundingMode: .plain)
     }
 
     var asMgdL: Decimal {
-        self / GlucoseUnits.exchangeRate
+        FreeAPS.rounded(self / GlucoseUnits.exchangeRate, scale: 0, roundingMode: .plain)
+    }
+
+    var formattedAsMmolL: String {
+        NumberFormatter.glucoseFormatter.string(from: asMmolL as NSDecimalNumber) ?? "\(asMmolL)"
     }
 }
 
 extension Double {
     var asMmolL: Decimal {
-        Decimal(self) * GlucoseUnits.exchangeRate
+        FreeAPS.rounded(Decimal(self) * GlucoseUnits.exchangeRate, scale: 1, roundingMode: .plain)
     }
 
     var asMgdL: Decimal {
-        Decimal(self) / GlucoseUnits.exchangeRate
+        FreeAPS.rounded(Decimal(self) / GlucoseUnits.exchangeRate, scale: 0, roundingMode: .plain)
     }
+
+    var formattedAsMmolL: String {
+        NumberFormatter.glucoseFormatter.string(from: asMmolL as NSDecimalNumber) ?? "\(asMmolL)"
+    }
+}
+
+extension NumberFormatter {
+    static let glucoseFormatter: NumberFormatter = {
+        let formatter = NumberFormatter()
+        formatter.locale = Locale.current
+        formatter.numberStyle = .decimal
+        formatter.minimumFractionDigits = 1
+        formatter.maximumFractionDigits = 1
+        return formatter
+    }()
 }
 
 extension BloodGlucose: SavitzkyGolaySmoothable {

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

@@ -60,6 +60,7 @@ struct FreeAPSSettings: JSON, Equatable {
     var yGridLines: Bool = true
     var oneDimensionalGraph: Bool = false
     var rulerMarks: Bool = true
+    var displayForecastsAsLines: Bool = false
     var maxCarbs: Decimal = 250
     var maxFat: Decimal = 250
     var maxProtein: Decimal = 250
@@ -286,6 +287,10 @@ extension FreeAPSSettings: Decodable {
             settings.rulerMarks = rulerMarks
         }
 
+        if let displayForecastsAsLines = try? container.decode(Bool.self, forKey: .displayForecastsAsLines) {
+            settings.displayForecastsAsLines = displayForecastsAsLines
+        }
+
         if let overrideHbA1cUnit = try? container.decode(Bool.self, forKey: .overrideHbA1cUnit) {
             settings.overrideHbA1cUnit = overrideHbA1cUnit
         }

+ 8 - 8
FreeAPS/Sources/Modules/AutotuneConfig/View/AutotuneConfigRootView.swift

@@ -118,14 +118,14 @@ extension AutotuneConfig {
                             .foregroundColor(.red)
                     }
 
-                    Section {
-                        Button {
-                            replaceAlert = true
-                        }
-                        label: { Text("Save as your Normal Basal Rates") }
-                    } header: {
-                        Text("Replace Normal Basal")
-                    }
+                    /* Section {
+                         Button {
+                             replaceAlert = true
+                         }
+                         label: { Text("Save as your Normal Basal Rates") }
+                     } header: {
+                         Text("Replace Normal Basal")
+                     } */
                 }
             }
             .scrollContentBackground(.hidden).background(color)

+ 5 - 1
FreeAPS/Sources/Modules/Bolus/BolusDataFlow.swift

@@ -3,5 +3,9 @@ enum Bolus {
 }
 
 protocol BolusProvider: Provider {
-    func pumpSettings() -> PumpSettings
+    func getPumpSettings() async -> PumpSettings
+    func getBasalProfile() async -> [BasalProfileEntry]
+    func getCarbRatios() async -> CarbRatios
+    func getBGTarget() async -> BGTargets
+    func getISFValues() async -> InsulinSensitivities
 }

+ 26 - 4
FreeAPS/Sources/Modules/Bolus/BolusProvider.swift

@@ -1,15 +1,37 @@
 extension Bolus {
     final class Provider: BaseProvider, BolusProvider {
-        func pumpSettings() -> PumpSettings {
-            storage.retrieve(OpenAPS.Settings.settings, as: PumpSettings.self)
+        func getPumpSettings() async -> PumpSettings {
+            await storage.retrieveAsync(OpenAPS.Settings.settings, as: PumpSettings.self)
                 ?? PumpSettings(from: OpenAPS.defaults(for: OpenAPS.Settings.settings))
                 ?? PumpSettings(insulinActionCurve: 6, maxBolus: 10, maxBasal: 2)
         }
 
-        func getProfile() -> [BasalProfileEntry] {
-            storage.retrieve(OpenAPS.Settings.basalProfile, as: [BasalProfileEntry].self)
+        func getBasalProfile() async -> [BasalProfileEntry] {
+            await storage.retrieveAsync(OpenAPS.Settings.basalProfile, as: [BasalProfileEntry].self)
                 ?? [BasalProfileEntry](from: OpenAPS.defaults(for: OpenAPS.Settings.basalProfile))
                 ?? []
         }
+
+        func getCarbRatios() async -> CarbRatios {
+            await storage.retrieveAsync(OpenAPS.Settings.carbRatios, as: CarbRatios.self)
+                ?? CarbRatios(from: OpenAPS.defaults(for: OpenAPS.Settings.carbRatios))
+                ?? CarbRatios(units: .grams, schedule: [])
+        }
+
+        func getBGTarget() async -> BGTargets {
+            await storage.retrieveAsync(OpenAPS.Settings.bgTargets, as: BGTargets.self)
+                ?? BGTargets(from: OpenAPS.defaults(for: OpenAPS.Settings.bgTargets))
+                ?? BGTargets(units: .mgdL, userPrefferedUnits: .mgdL, targets: [])
+        }
+
+        func getISFValues() async -> InsulinSensitivities {
+            await storage.retrieveAsync(OpenAPS.Settings.insulinSensitivities, as: InsulinSensitivities.self)
+                ?? InsulinSensitivities(from: OpenAPS.defaults(for: OpenAPS.Settings.insulinSensitivities))
+                ?? InsulinSensitivities(
+                    units: .mgdL,
+                    userPrefferedUnits: .mgdL,
+                    sensitivities: []
+                )
+        }
     }
 }

+ 308 - 192
FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift

@@ -17,6 +17,9 @@ extension Bolus {
         @Injected() var glucoseStorage: GlucoseStorage!
         @Injected() var determinationStorage: DeterminationStorage!
 
+        @Published var lowGlucose: Decimal = 70
+        @Published var highGlucose: Decimal = 180
+
         @Published var predictions: Predictions?
         @Published var amount: Decimal = 0
         @Published var insulinRecommended: Decimal = 0
@@ -25,6 +28,7 @@ extension Bolus {
         @Published var percentage: Decimal = 0
         @Published var threshold: Decimal = 0
         @Published var maxBolus: Decimal = 0
+        var maxExternal: Decimal { maxBolus * 3 }
         @Published var errorString: Decimal = 0
         @Published var evBG: Decimal = 0
         @Published var insulin: Decimal = 0
@@ -65,6 +69,10 @@ extension Bolus {
         @Published var displayPresets: Bool = true
 
         @Published var currentBasal: Decimal = 0
+        @Published var currentCarbRatio: Decimal = 0
+        @Published var currentBGTarget: Decimal = 0
+        @Published var currentISF: Decimal = 0
+
         @Published var sweetMeals: Bool = false
         @Published var sweetMealFactor: Decimal = 0
         @Published var useSuperBolus: Bool = false
@@ -84,6 +92,8 @@ extension Bolus {
         @Published var selection: MealPresetStored?
         @Published var summation: [String] = []
         @Published var maxCarbs: Decimal = 0
+        @Published var maxFat: Decimal = 0
+        @Published var maxProtein: Decimal = 0
 
         @Published var id_: String = ""
         @Published var summary: String = ""
@@ -93,10 +103,20 @@ extension Bolus {
         @Published var showInfo: Bool = false
         @Published var glucoseFromPersistence: [GlucoseStored] = []
         @Published var determination: [OrefDetermination] = []
+        @Published var preprocessedData: [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)] = []
+        @Published var predictionsForChart: Predictions?
+        @Published var simulatedDetermination: Determination?
+        @Published var determinationObjectIDs: [NSManagedObjectID] = []
+
+        @Published var minForecast: [Int] = []
+        @Published var maxForecast: [Int] = []
+        @Published var minCount: Int = 12 // count of Forecasts drawn in 5 min distances, i.e. 12 means a min of 1 hour
+        @Published var displayForecastsAsLines: Bool = false
+        @Published var smooth: Bool = false
 
         let now = Date.now
 
-        let context = CoreDataStack.shared.persistentContainer.viewContext
+        let viewContext = CoreDataStack.shared.persistentContainer.viewContext
         let backgroundContext = CoreDataStack.shared.newTaskContext()
 
         private var coreDataObserver: CoreDataObserver?
@@ -107,16 +127,24 @@ extension Bolus {
             setupGlucoseNotification()
             coreDataObserver = CoreDataObserver()
             registerHandlers()
-
             setupGlucoseArray()
-            setupDeterminationsArray()
+
+            Task {
+                async let getAllSettingsDefaults: () = getAllSettingsValues()
+                async let setupDeterminations: () = setupDeterminationsArray()
+
+                await getAllSettingsDefaults
+                await setupDeterminations
+
+                // Determination has updated, so we can use this to draw the initial Forecast Chart
+                let forecastData = await mapForecastsForChart()
+                await updateForecasts(with: forecastData)
+            }
 
             broadcaster.register(DeterminationObserver.self, observer: self)
             broadcaster.register(BolusFailureObserver.self, observer: self)
             units = settingsManager.settings.units
             percentage = settingsManager.settings.insulinReqPercentage
-            maxBolus = provider.pumpSettings().maxBolus
-            // added
             fraction = settings.settings.overrideFactor
             useCalc = settings.settings.useCalc
             fattyMeals = settings.settings.fattyMeals
@@ -125,9 +153,17 @@ extension Bolus {
             sweetMealFactor = settings.settings.sweetMealFactor
             displayPresets = settings.settings.displayPresets
 
+            displayForecastsAsLines = settings.settings.displayForecastsAsLines
+
+            lowGlucose = units == .mgdL ? settingsManager.settings.low : settingsManager.settings.low.asMmolL
+            highGlucose = units == .mgdL ? settingsManager.settings.high : settingsManager.settings.high.asMmolL
+
             maxCarbs = settings.settings.maxCarbs
+            maxFat = settings.settings.maxFat
+            maxProtein = settings.settings.maxProtein
             skipBolus = settingsManager.settings.skipBolusScreenAfterCarbs
             useFPUconversion = settingsManager.settings.useFPUconversion
+            smooth = settingsManager.settings.smoothGlucose
 
             if waitForSuggestionInitial {
                 Task {
@@ -143,47 +179,103 @@ extension Bolus {
 
         // MARK: - Basal
 
-        func getCurrentBasal() {
-            let basalEntries = provider.getProfile()
+        private enum SettingType {
+            case basal
+            case carbRatio
+            case bgTarget
+            case isf
+        }
+
+        func getAllSettingsValues() async {
+            await withTaskGroup(of: Void.self) { group in
+                group.addTask {
+                    await self.getCurrentSettingValue(for: .basal)
+                }
+                group.addTask {
+                    await self.getCurrentSettingValue(for: .carbRatio)
+                }
+                group.addTask {
+                    await self.getCurrentSettingValue(for: .bgTarget)
+                }
+                group.addTask {
+                    await self.getCurrentSettingValue(for: .isf)
+                }
+                group.addTask {
+                    let getMaxBolus = await self.provider.getPumpSettings().maxBolus
+                    await MainActor.run {
+                        self.maxBolus = getMaxBolus
+                    }
+                }
+            }
+        }
+
+        private func getCurrentSettingValue(for type: SettingType) async {
             let now = Date()
             let calendar = Calendar.current
             let dateFormatter = DateFormatter()
             dateFormatter.dateFormat = "HH:mm:ss"
             dateFormatter.timeZone = TimeZone.current
 
-            for (index, entry) in basalEntries.enumerated() {
+            let entries: [(start: String, value: Decimal)]
+
+            switch type {
+            case .basal:
+                let basalEntries = await provider.getBasalProfile()
+                entries = basalEntries.map { ($0.start, $0.rate) }
+            case .carbRatio:
+                let carbRatios = await provider.getCarbRatios()
+                entries = carbRatios.schedule.map { ($0.start, $0.ratio) }
+            case .bgTarget:
+                let bgTargets = await provider.getBGTarget()
+                entries = bgTargets.targets.map { ($0.start, $0.low) }
+            case .isf:
+                let isfValues = await provider.getISFValues()
+                entries = isfValues.sensitivities.map { ($0.start, $0.sensitivity) }
+            }
+
+            for (index, entry) in entries.enumerated() {
                 guard let entryTime = dateFormatter.date(from: entry.start) else {
                     print("Invalid entry start time: \(entry.start)")
                     continue
                 }
 
-                // Combine the current date with the time from entry.start
+                let entryComponents = calendar.dateComponents([.hour, .minute, .second], from: entryTime)
                 let entryStartTime = calendar.date(
-                    bySettingHour: calendar.component(.hour, from: entryTime),
-                    minute: calendar.component(.minute, from: entryTime),
-                    second: calendar.component(.second, from: entryTime),
+                    bySettingHour: entryComponents.hour!,
+                    minute: entryComponents.minute!,
+                    second: entryComponents.second!,
                     of: now
                 )!
 
                 let entryEndTime: Date
-                if index < basalEntries.count - 1,
-                   let nextEntryTime = dateFormatter.date(from: basalEntries[index + 1].start)
+                if index < entries.count - 1,
+                   let nextEntryTime = dateFormatter.date(from: entries[index + 1].start)
                 {
-                    let nextEntryStartTime = calendar.date(
-                        bySettingHour: calendar.component(.hour, from: nextEntryTime),
-                        minute: calendar.component(.minute, from: nextEntryTime),
-                        second: calendar.component(.second, from: nextEntryTime),
+                    let nextEntryComponents = calendar.dateComponents([.hour, .minute, .second], from: nextEntryTime)
+                    entryEndTime = calendar.date(
+                        bySettingHour: nextEntryComponents.hour!,
+                        minute: nextEntryComponents.minute!,
+                        second: nextEntryComponents.second!,
                         of: now
                     )!
-                    entryEndTime = nextEntryStartTime
                 } else {
-                    // If it's the last entry, use the same start time plus one day as the end time
                     entryEndTime = calendar.date(byAdding: .day, value: 1, to: entryStartTime)!
                 }
 
                 if now >= entryStartTime, now < entryEndTime {
-                    currentBasal = entry.rate
-                    break
+                    await MainActor.run {
+                        switch type {
+                        case .basal:
+                            currentBasal = entry.value
+                        case .carbRatio:
+                            currentCarbRatio = entry.value
+                        case .bgTarget:
+                            currentBGTarget = entry.value
+                        case .isf:
+                            currentISF = entry.value
+                        }
+                    }
+                    return
                 }
             }
         }
@@ -192,11 +284,7 @@ extension Bolus {
 
         /// Calculate insulin recommendation
         func calculateInsulin() -> Decimal {
-            // ensure that isf is in mg/dL
-            var conversion: Decimal {
-                units == .mmolL ? 0.0555 : 1
-            }
-            let isfForCalculation = isf / conversion
+            let isfForCalculation = units == .mmolL ? isf.asMgdL : isf
 
             // insulin needed for the current blood glucose
             targetDifference = currentBG - target
@@ -252,9 +340,11 @@ extension Bolus {
 
         // MARK: - Button tasks
 
-        @MainActor func invokeTreatmentsTask() {
+        func invokeTreatmentsTask() {
             Task {
-                addButtonPressed = true
+                await MainActor.run {
+                    self.addButtonPressed = true
+                }
                 let isInsulinGiven = amount > 0
                 let isCarbsPresent = carbs > 0
                 let isFatPresent = fat > 0
@@ -263,7 +353,9 @@ extension Bolus {
                 if isInsulinGiven {
                     try await handleInsulin(isExternal: externalInsulin)
                 } else if isCarbsPresent || isFatPresent || isProteinPresent {
-                    waitForSuggestion = true
+                    await MainActor.run {
+                        self.waitForSuggestion = true
+                    }
                 } else {
                     hideModal()
                     return
@@ -272,24 +364,28 @@ extension Bolus {
                 await saveMeal()
 
                 // if glucose data is stale end the custom loading animation by hiding the modal
-//                guard glucoseOfLast20Min.first?.date ?? now >= Date().addingTimeInterval(-12.minutes.timeInterval) else {
-//                    return hideModal()
-//                }
+                guard glucoseStorage.isGlucoseDataFresh(glucoseFromPersistence.first?.date) else {
+                    waitForSuggestion = false
+                    return hideModal()
+                }
             }
         }
 
         // MARK: - Insulin
 
-        @MainActor private func handleInsulin(isExternal: Bool) async throws {
+        private func handleInsulin(isExternal: Bool) async throws {
             if !isExternal {
                 await addPumpInsulin()
             } else {
                 await addExternalInsulin()
             }
-            waitForSuggestion = true
+
+            await MainActor.run {
+                self.waitForSuggestion = true
+            }
         }
 
-        @MainActor func addPumpInsulin() async {
+        func addPumpInsulin() async {
             guard amount > 0 else {
                 showModal(for: nil)
                 return
@@ -306,7 +402,7 @@ extension Bolus {
                 }
             } catch {
                 print("authentication error for pump bolus: \(error.localizedDescription)")
-                DispatchQueue.main.async {
+                await MainActor.run {
                     self.waitForSuggestion = false
                     if self.addButtonPressed {
                         self.hideModal()
@@ -315,38 +411,17 @@ extension Bolus {
             }
         }
 
-        private func savePumpInsulin(amount _: Decimal) {
-            context.perform {
-                // create pump event
-                let newPumpEvent = PumpEventStored(context: self.context)
-                newPumpEvent.timestamp = Date()
-                newPumpEvent.type = PumpEvent.bolus.rawValue
-
-                // create bolus entry and specify relationship to pump event
-                let newBolusEntry = BolusStored(context: self.context)
-                newBolusEntry.pumpEvent = newPumpEvent
-                newBolusEntry.amount = self.amount as NSDecimalNumber
-                newBolusEntry.isExternal = false
-                newBolusEntry.isSMB = false
-
-                do {
-                    guard self.context.hasChanges else { return }
-                    try self.context.save()
-                } catch {
-                    print(error.localizedDescription)
-                }
-            }
-        }
-
         // MARK: - EXTERNAL INSULIN
 
-        @MainActor func addExternalInsulin() async {
+        func addExternalInsulin() async {
             guard amount > 0 else {
                 showModal(for: nil)
                 return
             }
 
-            amount = min(amount, maxBolus * 3)
+            await MainActor.run {
+                self.amount = min(self.amount, self.maxBolus * 3)
+            }
 
             do {
                 let authenticated = try await unlockmanager.unlock()
@@ -360,7 +435,7 @@ extension Bolus {
                 }
             } catch {
                 print("authentication error for external insulin: \(error.localizedDescription)")
-                DispatchQueue.main.async {
+                await MainActor.run {
                     self.waitForSuggestion = false
                     if self.addButtonPressed {
                         self.hideModal()
@@ -371,10 +446,15 @@ extension Bolus {
 
         // MARK: - Carbs
 
-        @MainActor func saveMeal() async {
+        func saveMeal() async {
             guard carbs > 0 || fat > 0 || protein > 0 else { return }
-            carbs = min(carbs, maxCarbs)
-            id_ = UUID().uuidString
+
+            await MainActor.run {
+                self.carbs = min(self.carbs, self.maxCarbs)
+                self.fat = min(self.fat, self.maxFat)
+                self.protein = min(self.protein, self.maxProtein)
+                self.id_ = UUID().uuidString
+            }
 
             let carbsToStore = [CarbsEntry(
                 id: id_,
@@ -401,11 +481,11 @@ extension Bolus {
 
         func deletePreset() {
             if selection != nil {
-                context.delete(selection!)
+                viewContext.delete(selection!)
 
                 do {
-                    guard context.hasChanges else { return }
-                    try context.save()
+                    guard viewContext.hasChanges else { return }
+                    try viewContext.save()
                 } catch {
                     print(error.localizedDescription)
                 }
@@ -437,79 +517,6 @@ extension Bolus {
         func addToSummation() {
             summation.append(selection?.dish ?? "")
         }
-
-        func waitersNotepad() -> String {
-            var filteredArray = summation.filter { !$0.isEmpty }
-
-            if carbs == 0, protein == 0, fat == 0 {
-                filteredArray = []
-            }
-
-            guard filteredArray != [] else {
-                return ""
-            }
-            var carbs_: Decimal = 0.0
-            var fat_: Decimal = 0.0
-            var protein_: Decimal = 0.0
-            var presetArray = [MealPresetStored]()
-
-            context.performAndWait {
-                let requestPresets = MealPresetStored.fetchRequest() as NSFetchRequest<MealPresetStored>
-                try? presetArray = context.fetch(requestPresets)
-            }
-            var waitersNotepad = [String]()
-            var stringValue = ""
-
-            for each in filteredArray {
-                let countedSet = NSCountedSet(array: filteredArray)
-                let count = countedSet.count(for: each)
-                if each != stringValue {
-                    waitersNotepad.append("\(count) \(each)")
-                }
-                stringValue = each
-
-                for sel in presetArray {
-                    if sel.dish == each {
-                        carbs_ += (sel.carbs)! as Decimal
-                        fat_ += (sel.fat)! as Decimal
-                        protein_ += (sel.protein)! as Decimal
-                        break
-                    }
-                }
-            }
-            let extracarbs = carbs - carbs_
-            let extraFat = fat - fat_
-            let extraProtein = protein - protein_
-            var addedString = ""
-
-            if extracarbs > 0, filteredArray.isNotEmpty {
-                addedString += "Additional carbs: \(extracarbs) ,"
-            } else if extracarbs < 0 { addedString += "Removed carbs: \(extracarbs) " }
-
-            if extraFat > 0, filteredArray.isNotEmpty {
-                addedString += "Additional fat: \(extraFat) ,"
-            } else if extraFat < 0 { addedString += "Removed fat: \(extraFat) ," }
-
-            if extraProtein > 0, filteredArray.isNotEmpty {
-                addedString += "Additional protein: \(extraProtein) ,"
-            } else if extraProtein < 0 { addedString += "Removed protein: \(extraProtein) ," }
-
-            if addedString != "" {
-                waitersNotepad.append(addedString)
-            }
-            var waitersNotepadString = ""
-
-            if waitersNotepad.count == 1 {
-                waitersNotepadString = waitersNotepad[0]
-            } else if waitersNotepad.count > 1 {
-                for each in waitersNotepad {
-                    if each != waitersNotepad.last {
-                        waitersNotepadString += " " + each + ","
-                    } else { waitersNotepadString += " " + each }
-                }
-            }
-            return waitersNotepadString
-        }
     }
 }
 
@@ -537,7 +544,10 @@ extension Bolus.StateModel {
     private func registerHandlers() {
         coreDataObserver?.registerHandler(for: "OrefDetermination") { [weak self] in
             guard let self = self else { return }
-            self.setupDeterminationsArray()
+            Task {
+                await self.setupDeterminationsArray()
+                await self.updateForecasts()
+            }
         }
 
         // Due to the Batch insert this only is used for observing Deletion of Glucose entries
@@ -569,7 +579,8 @@ extension Bolus.StateModel {
     private func setupGlucoseArray() {
         Task {
             let ids = await self.fetchGlucose()
-            await updateGlucoseArray(with: ids)
+            let glucoseObjects: [GlucoseStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
+            await updateGlucoseArray(with: glucoseObjects)
         }
     }
 
@@ -577,72 +588,177 @@ extension Bolus.StateModel {
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             onContext: backgroundContext,
-            predicate: NSPredicate.predicateFor30MinAgo,
+            predicate: NSPredicate.glucose,
             key: "date",
             ascending: false,
-            fetchLimit: 3
+            fetchLimit: 288
         )
 
+        guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+
         return await backgroundContext.perform {
-            return results.map(\.objectID)
+            return fetchedResults.map(\.objectID)
         }
     }
 
-    @MainActor private func updateGlucoseArray(with IDs: [NSManagedObjectID]) {
-        do {
-            let glucoseObjects = try IDs.compactMap { id in
-                try context.existingObject(with: id) as? GlucoseStored
-            }
-            glucoseFromPersistence = glucoseObjects
+    @MainActor private func updateGlucoseArray(with objects: [GlucoseStored]) {
+        glucoseFromPersistence = objects
 
-            let lastGlucose = glucoseFromPersistence.first?.glucose ?? 0
-            let thirdLastGlucose = glucoseFromPersistence.last?.glucose ?? 0
-            let delta = Decimal(lastGlucose) - Decimal(thirdLastGlucose)
+        let lastGlucose = glucoseFromPersistence.first?.glucose ?? 0
+        let thirdLastGlucose = glucoseFromPersistence.dropFirst(2).first?.glucose ?? 0
+        let delta = Decimal(lastGlucose) - Decimal(thirdLastGlucose)
 
-            currentBG = Decimal(lastGlucose)
-            deltaBG = delta
-        } catch {
-            debugPrint(
-                "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the glucose array: \(error.localizedDescription)"
-            )
-        }
+        currentBG = Decimal(lastGlucose)
+        deltaBG = delta
     }
 
     // Determinations
-    private func setupDeterminationsArray() {
-        Task {
-            let ids = await determinationStorage.fetchLastDeterminationObjectID(
-                predicate: NSPredicate.predicateFor30MinAgoForDetermination
-            )
-            await updateDeterminationsArray(with: ids)
+    private func setupDeterminationsArray() async {
+        // Fetch object IDs on a background thread
+        let fetchedObjectIDs = await determinationStorage.fetchLastDeterminationObjectID(
+            predicate: NSPredicate.predicateFor30MinAgoForDetermination
+        )
+
+        // Update determinationObjectIDs on the main thread
+        await MainActor.run {
+            determinationObjectIDs = fetchedObjectIDs
         }
+
+        let determinationObjects: [OrefDetermination] = await CoreDataStack.shared
+            .getNSManagedObject(with: determinationObjectIDs, context: viewContext)
+
+        await updateDeterminationsArray(with: determinationObjects)
     }
 
-    @MainActor private func updateDeterminationsArray(with IDs: [NSManagedObjectID]) {
-        do {
-            let determinationObjects = try IDs.compactMap { id in
-                try context.existingObject(with: id) as? OrefDetermination
+    private func mapForecastsForChart() async -> Determination? {
+        let determinationObjects: [OrefDetermination] = await CoreDataStack.shared
+            .getNSManagedObject(with: determinationObjectIDs, context: backgroundContext)
+
+        return await backgroundContext.perform {
+            guard let determinationObject = determinationObjects.first else {
+                return nil
             }
-            guard let mostRecentDetermination = determinationObjects.first else { return }
-            determination = determinationObjects
-
-            // setup vars for bolus calculation
-            insulinRequired = (mostRecentDetermination.insulinReq ?? 0) as Decimal
-            evBG = (mostRecentDetermination.eventualBG ?? 0) as Decimal
-            insulin = (mostRecentDetermination.insulinForManualBolus ?? 0) as Decimal
-            target = (mostRecentDetermination.currentTarget ?? 100) as Decimal
-            isf = (mostRecentDetermination.insulinSensitivity ?? 0) as Decimal
-            cob = mostRecentDetermination.cob as Int16
-            iob = (mostRecentDetermination.iob ?? 0) as Decimal
-            basal = (mostRecentDetermination.tempBasal ?? 0) as Decimal
-            carbRatio = (mostRecentDetermination.carbRatio ?? 0) as Decimal
-
-            getCurrentBasal()
-            insulinCalculated = calculateInsulin()
-        } catch {
-            debugPrint(
-                "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the determinations array: \(error.localizedDescription)"
+
+            let eventualBG = determinationObject.eventualBG?.intValue
+
+            let forecastsSet = determinationObject.forecasts as? Set<Forecast> ?? []
+            let predictions = Predictions(
+                iob: forecastsSet.extractValues(for: "iob"),
+                zt: forecastsSet.extractValues(for: "zt"),
+                cob: forecastsSet.extractValues(for: "cob"),
+                uam: forecastsSet.extractValues(for: "uam")
+            )
+
+            return Determination(
+                id: UUID(),
+                reason: "",
+                units: 0,
+                insulinReq: 0,
+                eventualBG: eventualBG,
+                sensitivityRatio: 0,
+                rate: 0,
+                duration: 0,
+                iob: 0,
+                cob: 0,
+                predictions: predictions.isEmpty ? nil : predictions,
+                carbsReq: 0,
+                temp: nil,
+                bg: 0,
+                reservoir: 0,
+                isf: 0,
+                tdd: 0,
+                insulin: nil,
+                current_target: 0,
+                insulinForManualBolus: 0,
+                manualBolusErrorString: 0,
+                minDelta: 0,
+                expectedDelta: 0,
+                minGuardBG: 0,
+                minPredBG: 0,
+                threshold: 0,
+                carbRatio: 0,
+                received: false
             )
         }
     }
+
+    @MainActor private func updateDeterminationsArray(with objects: [OrefDetermination]) {
+        guard let mostRecentDetermination = objects.first else { return }
+        determination = objects
+
+        // setup vars for bolus calculation
+        insulinRequired = (mostRecentDetermination.insulinReq ?? 0) as Decimal
+        evBG = (mostRecentDetermination.eventualBG ?? 0) as Decimal
+        insulin = (mostRecentDetermination.insulinForManualBolus ?? 0) as Decimal
+        target = (mostRecentDetermination.currentTarget ?? currentBGTarget as NSDecimalNumber) as Decimal
+        isf = (mostRecentDetermination.insulinSensitivity ?? currentISF as NSDecimalNumber) as Decimal
+        cob = mostRecentDetermination.cob as Int16
+        iob = (mostRecentDetermination.iob ?? 0) as Decimal
+        basal = (mostRecentDetermination.tempBasal ?? 0) as Decimal
+        carbRatio = (mostRecentDetermination.carbRatio ?? currentCarbRatio as NSDecimalNumber) as Decimal
+        insulinCalculated = calculateInsulin()
+    }
+}
+
+extension Bolus.StateModel {
+    @MainActor func updateForecasts(with forecastData: Determination? = nil) async {
+        if let forecastData = forecastData {
+            simulatedDetermination = forecastData
+        } else {
+            simulatedDetermination = await Task.detached { [self] in
+                await apsManager.simulateDetermineBasal(carbs: carbs, iob: amount)
+            }.value
+        }
+
+        predictionsForChart = simulatedDetermination?.predictions
+
+        let nonEmptyArrays = [
+            predictionsForChart?.iob,
+            predictionsForChart?.zt,
+            predictionsForChart?.cob,
+            predictionsForChart?.uam
+        ]
+        .compactMap { $0 }
+        .filter { !$0.isEmpty }
+
+        guard !nonEmptyArrays.isEmpty else {
+            minForecast = []
+            maxForecast = []
+            return
+        }
+
+        minCount = max(12, nonEmptyArrays.map(\.count).min() ?? 0)
+        guard minCount > 0 else { return }
+
+        let (minResult, maxResult) = await Task.detached {
+            let minForecast = (0 ..< self.minCount).map { index in
+                nonEmptyArrays.compactMap { $0.indices.contains(index) ? $0[index] : nil }.min() ?? 0
+            }
+
+            let maxForecast = (0 ..< self.minCount).map { index in
+                nonEmptyArrays.compactMap { $0.indices.contains(index) ? $0[index] : nil }.max() ?? 0
+            }
+
+            return (minForecast, maxForecast)
+        }.value
+
+        minForecast = minResult
+        maxForecast = maxResult
+    }
+}
+
+private extension Set where Element == Forecast {
+    func extractValues(for type: String) -> [Int]? {
+        let values = first { $0.type == type }?
+            .forecastValues?
+            .sorted { $0.index < $1.index }
+            .compactMap { Int($0.value) }
+        return values?.isEmpty ?? true ? nil : values
+    }
+}
+
+private extension Predictions {
+    var isEmpty: Bool {
+        iob == nil && zt == nil && cob == nil && uam == nil
+    }
 }

+ 119 - 0
FreeAPS/Sources/Modules/Bolus/View/AddMealPresetView.swift

@@ -0,0 +1,119 @@
+import CoreData
+import Foundation
+import SwiftUI
+
+struct AddMealPresetView: View {
+    @Binding var dish: String
+    @Binding var presetCarbs: Decimal
+    @Binding var presetFat: Decimal
+    @Binding var presetProtein: Decimal
+    var onSave: () -> Void
+    var onCancel: () -> Void
+
+    @Environment(\.colorScheme) private var colorScheme
+    private var color: LinearGradient {
+        colorScheme == .dark ? LinearGradient(
+            gradient: Gradient(colors: [
+                Color.bgDarkBlue,
+                Color.bgDarkerDarkBlue
+            ]),
+            startPoint: .top,
+            endPoint: .bottom
+        )
+            :
+            LinearGradient(
+                gradient: Gradient(colors: [Color.gray.opacity(0.1)]),
+                startPoint: .top,
+                endPoint: .bottom
+            )
+    }
+
+    private var mealFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 1
+        return formatter
+    }
+
+    var body: some View {
+        NavigationStack {
+            Form {
+                Section {
+                    TextField("Name Of Dish", text: $dish)
+                } header: {
+                    Text("New Preset")
+                }
+                .listRowBackground(Color.chart)
+
+                Section {
+                    carbsTextField()
+                    proteinAndFat()
+                }
+                .listRowBackground(Color.chart)
+
+                savePresetButton
+            }
+            .scrollContentBackground(.hidden).background(color)
+            .navigationTitle("Add Meal Preset")
+            .navigationBarTitleDisplayMode(.inline)
+            .toolbar(content: {
+                ToolbarItem(placement: .topBarLeading) {
+                    Button {
+                        onCancel()
+                    } label: {
+                        Text("Cancel")
+                    }
+                }
+            })
+        }
+    }
+
+    @ViewBuilder private func carbsTextField() -> some View {
+        HStack {
+            Text("Carbs").fontWeight(.semibold)
+            Spacer()
+            TextFieldWithToolBar(
+                text: $presetCarbs,
+                placeholder: "0",
+                keyboardType: .numberPad,
+                numberFormatter: mealFormatter
+            )
+            Text("g").foregroundColor(.secondary)
+        }
+    }
+
+    @ViewBuilder private func proteinAndFat() -> some View {
+        HStack {
+            Text("Fat").foregroundColor(.orange)
+            Spacer()
+            TextFieldWithToolBar(text: $presetFat, placeholder: "0", keyboardType: .numberPad, numberFormatter: mealFormatter)
+            Text("g").foregroundColor(.secondary)
+        }
+        HStack {
+            Text("Protein").foregroundColor(.red)
+            Spacer()
+            TextFieldWithToolBar(
+                text: $presetProtein,
+                placeholder: "0",
+                keyboardType: .numberPad,
+                numberFormatter: mealFormatter
+            )
+            Text("g").foregroundColor(.secondary)
+        }
+    }
+
+    private var savePresetButton: some View {
+        Button {
+            onSave()
+        }
+        label: {
+            Text("Save")
+                .font(.headline)
+                .foregroundStyle(Color.white)
+                .frame(maxWidth: .infinity, alignment: .center)
+        }
+        .listRowBackground(Color(.systemBlue))
+        .shadow(radius: 3)
+        .clipShape(RoundedRectangle(cornerRadius: 8))
+    }
+}

+ 129 - 245
FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift

@@ -10,6 +10,7 @@ extension Bolus {
             case carbs
             case fat
             case protein
+            case bolus
         }
 
         @FocusState private var focusedField: FocusedField?
@@ -18,17 +19,12 @@ extension Bolus {
 
         @StateObject var state = StateModel()
 
-        @State private var showAlert = false
+        @State private var showPresetSheet = false
         @State private var autofocus: Bool = true
         @State private var calculatorDetent = PresentationDetent.medium
         @State private var pushed: Bool = false
-        @State private var isPromptPresented: Bool = false
-        @State private var dish: String = ""
-        @State private var saved: Bool = false
         @State private var debounce: DispatchWorkItem?
 
-        @Environment(\.managedObjectContext) var moc
-
         private enum Config {
             static let dividerHeight: CGFloat = 2
             static let spacing: CGFloat = 3
@@ -36,11 +32,6 @@ extension Bolus {
 
         @Environment(\.colorScheme) var colorScheme
 
-        @FetchRequest(
-            entity: MealPresetStored.entity(),
-            sortDescriptors: [NSSortDescriptor(key: "dish", ascending: true)]
-        ) var carbPresets: FetchedResults<MealPresetStored>
-
         private var formatter: NumberFormatter {
             let formatter = NumberFormatter()
             formatter.numberStyle = .decimal
@@ -87,179 +78,32 @@ extension Bolus {
                 )
         }
 
-        private var empty: Bool {
-            state.useFPUconversion ? (state.carbs <= 0 && state.fat <= 0 && state.protein <= 0) : (state.carbs <= 0)
-        }
-
         /// Handles macro input (carb, fat, protein) in a debounced fashion.
         func handleDebouncedInput() {
             debounce?.cancel()
             debounce = DispatchWorkItem { [self] in
                 state.insulinCalculated = state.calculateInsulin()
+                Task {
+                    await state.updateForecasts()
+                }
             }
             if let debounce = debounce {
                 DispatchQueue.main.asyncAfter(deadline: .now() + 0.35, execute: debounce)
             }
         }
 
-        private var presetPopover: some View {
-            Form {
-                Section {
-                    TextField("Name Of Dish", text: $dish)
-                    Button {
-                        saved = true
-                        if dish != "", saved {
-                            let preset = MealPresetStored(context: moc)
-                            preset.dish = dish
-                            preset.fat = state.fat as NSDecimalNumber
-                            preset.protein = state.protein as NSDecimalNumber
-                            preset.carbs = state.carbs as NSDecimalNumber
-                            if self.moc.hasChanges {
-                                try? moc.save()
-                            }
-                            state.addNewPresetToWaitersNotepad(dish)
-                            saved = false
-                            isPromptPresented = false
-                        }
-                    }
-                    label: { Text("Save") }
-                    Button {
-                        dish = ""
-                        saved = false
-                        isPromptPresented = false }
-                    label: { Text("Cancel") }
-                } header: { Text("Enter Meal Preset Name") }
-            }
-        }
-
-        private var minusButton: some View {
-            Button {
-                if state.carbs != 0,
-                   (state.carbs - (((state.selection?.carbs ?? 0) as NSDecimalNumber) as Decimal) as Decimal) >= 0
-                {
-                    state.carbs -= (((state.selection?.carbs ?? 0) as NSDecimalNumber) as Decimal)
-                } else { state.carbs = 0 }
-
-                if state.fat != 0,
-                   (state.fat - (((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal) as Decimal) >= 0
-                {
-                    state.fat -= (((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal)
-                } else { state.fat = 0 }
-
-                if state.protein != 0,
-                   (state.protein - (((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal) as Decimal) >= 0
-                {
-                    state.protein -= (((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal)
-                } else { state.protein = 0 }
-
-                state.removePresetFromNewMeal()
-                if state.carbs == 0, state.fat == 0, state.protein == 0 { state.summation = [] }
-            }
-            label: { Image(systemName: "minus.circle.fill")
-                .font(.system(size: 20))
-            }
-            .disabled(
-                state
-                    .selection == nil ||
-                    (
-                        !state.summation
-                            .contains(state.selection?.dish ?? "") && (state.selection?.dish ?? "") != ""
-                    )
-            )
-            .buttonStyle(.borderless)
-            .tint(.blue)
-        }
-
-        private var plusButton: some View {
-            Button {
-                state.carbs += ((state.selection?.carbs ?? 0) as NSDecimalNumber) as Decimal
-                state.fat += ((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal
-                state.protein += ((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal
-
-                state.addPresetToNewMeal()
-            }
-            label: { Image(systemName: "plus.circle.fill")
-                .font(.system(size: 20))
-            }
-            .disabled(state.selection == nil)
-            .buttonStyle(.borderless)
-            .tint(.blue)
-        }
-
-        private var mealPresets: some View {
-            Section {
-                HStack {
-                    if state.selection != nil {
-                        minusButton
-                    }
-                    Picker("Preset", selection: $state.selection) {
-                        Text("Saved Food").tag(nil as MealPresetStored?)
-                        ForEach(carbPresets, id: \.self) { (preset: MealPresetStored) in
-                            Text(preset.dish ?? "").tag(preset as MealPresetStored?)
-                        }
-                    }
-                    .labelsHidden()
-                    .frame(maxWidth: .infinity, alignment: .center)
-                    ._onBindingChange($state.selection) { _ in
-                        state.carbs += ((state.selection?.carbs ?? 0) as NSDecimalNumber) as Decimal
-                        state.fat += ((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal
-                        state.protein += ((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal
-                        state.addToSummation()
-                    }
-                    if state.selection != nil {
-                        plusButton
-                    }
-                }
-
-                HStack {
-                    Button("Delete Preset") {
-                        showAlert.toggle()
-                    }
-                    .disabled(state.selection == nil)
-                    .tint(.orange)
-                    .buttonStyle(.borderless)
-                    .alert(
-                        "Delete preset '\(state.selection?.dish ?? "")'?",
-                        isPresented: $showAlert,
-                        actions: {
-                            Button("No", role: .cancel) {}
-                            Button("Yes", role: .destructive) {
-                                state.deletePreset()
-
-                                state.carbs += ((state.selection?.carbs ?? 0) as NSDecimalNumber) as Decimal
-                                state.fat += ((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal
-                                state.protein += ((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal
-
-                                state.addPresetToNewMeal()
-                            }
-                        }
-                    )
-
-                    Spacer()
-
-                    Button {
-                        isPromptPresented = true
-                    }
-                    label: { Text("Save as Preset") }
-                        .buttonStyle(.borderless)
-                        .disabled(
-                            empty ||
-                                (
-                                    (((state.selection?.carbs ?? 0) as NSDecimalNumber) as Decimal) == state
-                                        .carbs && (((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal) == state
-                                        .fat && (((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal) == state
-                                        .protein
-                                )
-                        )
-                }
-            }
-        }
-
         @ViewBuilder private func proteinAndFat() -> some View {
             HStack {
                 Text("Fat").foregroundColor(.orange)
                 Spacer()
-                TextFieldWithToolBar(text: $state.fat, placeholder: "0", keyboardType: .numberPad, numberFormatter: mealFormatter)
+                TextFieldWithToolBar(
+                    text: $state.fat,
+                    placeholder: "0",
+                    keyboardType: .numberPad,
+                    numberFormatter: mealFormatter,
+                    previousTextField: { focusOnPreviousTextField(index: 2) },
+                    nextTextField: { focusOnNextTextField(index: 2) }
+                ).focused($focusedField, equals: .fat)
                 Text("g").foregroundColor(.secondary)
             }
             HStack {
@@ -269,8 +113,10 @@ extension Bolus {
                     text: $state.protein,
                     placeholder: "0",
                     keyboardType: .numberPad,
-                    numberFormatter: mealFormatter
-                )
+                    numberFormatter: mealFormatter,
+                    previousTextField: { focusOnPreviousTextField(index: 3) },
+                    nextTextField: { focusOnNextTextField(index: 3) }
+                ).focused($focusedField, equals: .protein)
                 Text("g").foregroundColor(.secondary)
             }
         }
@@ -283,15 +129,43 @@ extension Bolus {
                     text: $state.carbs,
                     placeholder: "0",
                     keyboardType: .numberPad,
-                    numberFormatter: mealFormatter
-                )
-                .onChange(of: state.carbs) { _ in
-                    handleDebouncedInput()
-                }
+                    numberFormatter: mealFormatter,
+                    previousTextField: { focusOnPreviousTextField(index: 1) },
+                    nextTextField: { focusOnNextTextField(index: 1) }
+                ).focused($focusedField, equals: .carbs)
+                    .onChange(of: state.carbs) { _ in
+                        handleDebouncedInput()
+                    }
                 Text("g").foregroundColor(.secondary)
             }
         }
 
+        func focusOnPreviousTextField(index: Int) {
+            switch index {
+            case 2:
+                focusedField = .carbs
+            case 3:
+                focusedField = .fat
+            case 4:
+                focusedField = .protein
+            default:
+                break
+            }
+        }
+
+        func focusOnNextTextField(index: Int) {
+            switch index {
+            case 1:
+                focusedField = .fat
+            case 2:
+                focusedField = .protein
+            case 3:
+                focusedField = .bolus
+            default:
+                break
+            }
+        }
+
         var body: some View {
             ZStack(alignment: .center) {
                 VStack {
@@ -303,20 +177,6 @@ extension Bolus {
                                 proteinAndFat()
                             }
 
-                            // Summary when combining presets
-                            if state.waitersNotepad() != "" {
-                                HStack {
-                                    Text("Total")
-                                    let test = state.waitersNotepad().components(separatedBy: ", ").removeDublicates()
-                                    HStack(spacing: 0) {
-                                        ForEach(test, id: \.self) {
-                                            Text($0).foregroundStyle(Color.blue).font(.footnote)
-                                            Text($0 == test[test.count - 1] ? "" : ", ")
-                                        }
-                                    }.frame(maxWidth: .infinity, alignment: .trailing)
-                                }
-                            }
-
                             // Time
                             HStack {
                                 Text("Time").foregroundStyle(Color.secondary)
@@ -341,18 +201,12 @@ extension Bolus {
                                     label: { Image(systemName: "plus.circle") }.tint(.blue).buttonStyle(.borderless)
                                 }
                             }
-
-                            .popover(isPresented: $isPromptPresented) {
-                                presetPopover
+                            HStack {
+                                Image(systemName: "square.and.pencil").foregroundColor(.secondary)
+                                TextFieldWithToolBarString(text: $state.note, placeholder: "", maxLength: 25)
                             }
                         }.listRowBackground(Color.chart)
 
-                        if state.displayPresets {
-                            Section {
-                                mealPresets
-                            }.listRowBackground(Color.chart)
-                        }
-
                         Section {
                             HStack {
                                 Button(action: {
@@ -420,19 +274,29 @@ extension Bolus {
                                     placeholder: "0",
                                     textColor: colorScheme == .dark ? .white : .blue,
                                     maxLength: 5,
-                                    numberFormatter: formatter
-                                )
+                                    numberFormatter: formatter,
+                                    previousTextField: { focusOnPreviousTextField(index: 4) },
+                                    nextTextField: { focusOnNextTextField(index: 4) }
+                                ).focused($focusedField, equals: .bolus)
+                                    .onChange(of: state.amount) { _ in
+                                        Task {
+                                            await state.updateForecasts()
+                                        }
+                                    }
                                 Text(" U").foregroundColor(.secondary)
                             }
 
-                            if state.amount > 0 {
-                                HStack {
-                                    Text("External insulin")
-                                    Spacer()
-                                    Toggle("", isOn: $state.externalInsulin).toggleStyle(Checkbox())
-                                }
+                            HStack {
+                                Text("External insulin")
+                                Spacer()
+                                Toggle("", isOn: $state.externalInsulin).toggleStyle(Checkbox())
                             }
                         }.listRowBackground(Color.chart)
+
+                        Section {
+                            ForeCastChart(state: state, units: $state.units)
+                                .padding(.vertical)
+                        }.listRowBackground(Color.chart)
                     }
                 }
                 .safeAreaInset(edge: .bottom, spacing: 0) {
@@ -455,6 +319,16 @@ extension Bolus {
                         Text("Close")
                     }
                 }
+                ToolbarItem(placement: .topBarTrailing) {
+                    Button(action: {
+                        showPresetSheet = true
+                    }, label: {
+                        HStack {
+                            Text("Presets")
+                            Image(systemName: "plus")
+                        }
+                    })
+                }
             })
             .onAppear {
                 configureView {
@@ -471,6 +345,11 @@ extension Bolus {
                         selection: $calculatorDetent
                     )
             }
+            .sheet(isPresented: $showPresetSheet, onDismiss: {
+                showPresetSheet = false
+            }) {
+                MealPresetView(state: state)
+            }
         }
 
         var progressText: ProgressText {
@@ -507,10 +386,7 @@ extension Bolus {
                         .frame(minHeight: 50)
                 }
                 .disabled(disableTaskButton)
-                .background(
-                    (state.externalInsulin ? externalBolusLimit : pumpBolusLimit) ? Color(.systemRed) :
-                        Color(.systemBlue)
-                )
+                .background(limitExceeded ? Color(.systemRed) : Color(.systemBlue))
                 .shadow(radius: 3)
                 .clipShape(RoundedRectangle(cornerRadius: 8))
                 .padding()
@@ -519,61 +395,69 @@ extension Bolus {
         }
 
         private var taskButtonLabel: some View {
+            if pumpBolusLimitExceeded {
+                return Text("Max Bolus of \(state.maxBolus) U Exceeded")
+            } else if externalBolusLimitExceeded {
+                return Text("Max External Bolus of \(state.maxExternal) U Exceeded")
+            } else if carbLimitExceeded {
+                return Text("Max Carbs of \(state.maxCarbs) g Exceeded")
+            } else if fatLimitExceeded {
+                return Text("Max Fat of \(state.maxFat) g Exceeded")
+            } else if proteinLimitExceeded {
+                return Text("Max Protein of \(state.maxProtein) g Exceeded")
+            }
+
             let hasInsulin = state.amount > 0
             let hasCarbs = state.carbs > 0
             let hasFatOrProtein = state.fat > 0 || state.protein > 0
+            let bolusString = state.externalInsulin ? "External Insulin" : "Enact Bolus"
 
             switch (hasInsulin, hasCarbs, hasFatOrProtein) {
             case (true, true, true):
-                return Text(
-                    state
-                        .externalInsulin ? (
-                            externalBolusLimit ? "Manual bolus exceeds max bolus!" : "Log meal and external insulin"
-                        ) :
-                        (pumpBolusLimit ? "Pump bolus exceeds max bolus!" : "Log meal and enact bolus")
-                )
+                return Text("Log Meal and \(bolusString)")
             case (true, true, false):
-                return Text(
-                    state
-                        .externalInsulin ?
-                        (externalBolusLimit ? "Manual bolus exceeds max bolus!" : "Log carbs and external insulin") :
-                        (pumpBolusLimit ? "Pump bolus exceeds max bolus!" : "Log carbs and enact bolus")
-                )
+                return Text("Log Carbs and \(bolusString)")
             case (true, false, true):
-                return Text(
-                    state
-                        .externalInsulin ?
-                        (externalBolusLimit ? "Manual bolus exceeds max bolus!" : "Log FPUs and external insulin") :
-                        (pumpBolusLimit ? "Pump bolus exceeds max bolus!" : "Log FPUs and enact bolus")
-                )
+                return Text("Log FPU and \(bolusString)")
             case (true, false, false):
-                return Text(
-                    state
-                        .externalInsulin ? (externalBolusLimit ? "Manual bolus exceeds max bolus!" : "Log external insulin") :
-                        (pumpBolusLimit ? "Pump bolus exceeds max bolus!" : "Enact bolus")
-                )
+                return Text(state.externalInsulin ? "Log External Insulin" : "Enact Bolus")
             case (false, true, true):
-                return Text("Log meal")
+                return Text("Log Meal")
             case (false, true, false):
-                return Text("Log carbs")
+                return Text("Log Carbs")
             case (false, false, true):
-                return Text("Log FPUs")
+                return Text("Log FPU")
             default:
-                return Text("Continue without treatment")
+                return Text("Continue Without Treatment")
             }
         }
 
-        private var pumpBolusLimit: Bool {
-            state.amount > state.maxBolus
+        private var pumpBolusLimitExceeded: Bool {
+            !state.externalInsulin && state.amount > state.maxBolus
+        }
+
+        private var externalBolusLimitExceeded: Bool {
+            state.externalInsulin && state.amount > state.maxExternal
+        }
+
+        private var carbLimitExceeded: Bool {
+            state.carbs > state.maxCarbs
+        }
+
+        private var fatLimitExceeded: Bool {
+            state.fat > state.maxFat
+        }
+
+        private var proteinLimitExceeded: Bool {
+            state.protein > state.maxProtein
         }
 
-        private var externalBolusLimit: Bool {
-            state.amount > state.maxBolus * 3
+        private var limitExceeded: Bool {
+            pumpBolusLimitExceeded || externalBolusLimitExceeded || carbLimitExceeded || fatLimitExceeded || proteinLimitExceeded
         }
 
         private var disableTaskButton: Bool {
-            state.addButtonPressed ||
-                (state.amount > 0 ? (state.externalInsulin ? externalBolusLimit : pumpBolusLimit) : false)
+            state.addButtonPressed || limitExceeded
         }
     }
 

+ 255 - 0
FreeAPS/Sources/Modules/Bolus/View/ForeCastChart.swift

@@ -0,0 +1,255 @@
+import Charts
+import CoreData
+import Foundation
+import SwiftUI
+
+struct ForeCastChart: View {
+    @StateObject var state: Bolus.StateModel
+    @Environment(\.colorScheme) var colorScheme
+    @Binding var units: GlucoseUnits
+
+    @State private var startMarker = Date(timeIntervalSinceNow: -4 * 60 * 60)
+
+    private var endMarker: Date {
+        state
+            .displayForecastsAsLines ? Date(timeIntervalSinceNow: TimeInterval(hours: 3)) :
+            Date(timeIntervalSinceNow: TimeInterval(
+                Int(1.5) * 5 * state
+                    .minCount * 60
+            )) // min is 1.5h -> (1.5*1h = 1.5*(5*12*60))
+    }
+
+    private var glucoseFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+
+        if units == .mmolL {
+            formatter.maximumFractionDigits = 1
+            formatter.minimumFractionDigits = 1
+            formatter.roundingMode = .halfUp
+        } else {
+            formatter.maximumFractionDigits = 0
+        }
+        return formatter
+    }
+
+    var body: some View {
+        VStack {
+            forecastChart
+                .padding(.vertical, 3)
+            HStack {
+                Spacer()
+                Image(systemName: "arrow.right.circle")
+                    .font(.system(size: 16, weight: .bold))
+
+                if let eventualBG = state.simulatedDetermination?.eventualBG {
+                    HStack {
+                        Text(
+                            units == .mgdL ? Decimal(eventualBG).description : eventualBG.formattedAsMmolL
+                        )
+                        .font(.footnote)
+                        .foregroundStyle(.primary)
+                        Text("\(units.rawValue)")
+                            .font(.footnote)
+                            .foregroundStyle(.secondary)
+                    }
+                } else {
+                    Text("---")
+                        .font(.footnote)
+                        .foregroundStyle(.primary)
+                    Text("\(units.rawValue)")
+                        .font(.footnote)
+                        .foregroundStyle(.secondary)
+                }
+            }
+        }
+    }
+
+    private var forecastChart: some View {
+        Chart {
+            drawGlucose()
+            drawCurrentTimeMarker()
+
+            if state.displayForecastsAsLines {
+                drawForecastLines()
+            } else {
+                drawForecastsCone()
+            }
+        }
+        .chartXAxis { forecastChartXAxis }
+        .chartXScale(domain: startMarker ... endMarker)
+        .chartYAxis { forecastChartYAxis }
+        .chartYScale(domain: units == .mgdL ? 0 ... 300 : 0.asMmolL ... 300.asMmolL)
+        .backport.chartForegroundStyleScale(state: state)
+    }
+
+    private var stops: [Gradient.Stop] {
+        let low = Double(state.lowGlucose)
+        let high = Double(state.highGlucose)
+
+        let glucoseValues = state.glucoseFromPersistence
+            .map { units == .mgdL ? Decimal($0.glucose) : Decimal($0.glucose).asMmolL }
+
+        let minimum = glucoseValues.min() ?? 0.0
+        let maximum = glucoseValues.max() ?? 0.0
+
+        // Calculate positions for gradient
+        let lowPosition = (low - Double(truncating: minimum as NSNumber)) /
+            (Double(truncating: maximum as NSNumber) - Double(truncating: minimum as NSNumber))
+        let highPosition = (high - Double(truncating: minimum as NSNumber)) /
+            (Double(truncating: maximum as NSNumber) - Double(truncating: minimum as NSNumber))
+
+        // Ensure positions are in bounds [0, 1]
+        let clampedLowPosition = max(0.0, min(lowPosition, 1.0))
+        let clampedHighPosition = max(0.0, min(highPosition, 1.0))
+
+        // Ensure lowPosition is less than highPosition
+        let sortedPositions = [clampedLowPosition, clampedHighPosition].sorted()
+
+        return [
+            Gradient.Stop(color: .red, location: 0.0),
+            Gradient.Stop(color: .red, location: sortedPositions[0]), // draw red gradient till lowGlucose
+            Gradient.Stop(color: .green, location: sortedPositions[0] + 0.0001), // draw green above lowGlucose till highGlucose
+            Gradient.Stop(color: .green, location: sortedPositions[1]),
+            Gradient.Stop(color: .orange, location: sortedPositions[1] + 0.0001), // draw orange above highGlucose
+            Gradient.Stop(color: .orange, location: 1.0)
+        ]
+    }
+
+    private func drawGlucose() -> some ChartContent {
+        ForEach(state.glucoseFromPersistence) { item in
+            let glucoseToDisplay = units == .mgdL ? Decimal(item.glucose) : Decimal(item.glucose).asMmolL
+
+            if state.smooth {
+                LineMark(
+                    x: .value("Time", item.date ?? Date()),
+                    y: .value("Value", glucoseToDisplay)
+                )
+                .foregroundStyle(
+                    .linearGradient(stops: stops, startPoint: .bottom, endPoint: .top)
+                )
+                .symbol(.circle).symbolSize(34)
+            } else {
+                if item.glucose > Int(state.highGlucose) {
+                    PointMark(
+                        x: .value("Time", item.date ?? Date(), unit: .second),
+                        y: .value("Value", glucoseToDisplay)
+                    )
+                    .foregroundStyle(Color.orange.gradient)
+                    .symbolSize(20)
+                } else if item.glucose < Int(state.lowGlucose) {
+                    PointMark(
+                        x: .value("Time", item.date ?? Date(), unit: .second),
+                        y: .value("Value", glucoseToDisplay)
+                    )
+                    .foregroundStyle(Color.red.gradient)
+                    .symbolSize(20)
+                } else {
+                    PointMark(
+                        x: .value("Time", item.date ?? Date(), unit: .second),
+                        y: .value("Value", glucoseToDisplay)
+                    )
+                    .foregroundStyle(Color.green.gradient)
+                    .symbolSize(20)
+                }
+            }
+        }
+    }
+
+    private func timeForIndex(_ index: Int32) -> Date {
+        let currentTime = Date()
+        let timeInterval = TimeInterval(index * 300)
+        return currentTime.addingTimeInterval(timeInterval)
+    }
+
+    private func drawForecastsCone() -> some ChartContent {
+        // Draw AreaMark for the forecast bounds
+        ForEach(0 ..< max(state.minForecast.count, state.maxForecast.count), id: \.self) { index in
+            if index < state.minForecast.count, index < state.maxForecast.count {
+                let yMinMaxDelta = Decimal(state.minForecast[index] - state.maxForecast[index])
+                let xValue = timeForIndex(Int32(index))
+
+                // if distance between respective min and max is 0, provide a default range
+                if yMinMaxDelta == 0 {
+                    let yMinValue = units == .mgdL ? Decimal(state.minForecast[index] - 1) :
+                        Decimal(state.minForecast[index] - 1)
+                        .asMmolL
+                    let yMaxValue = units == .mgdL ? Decimal(state.minForecast[index] + 1) :
+                        Decimal(state.minForecast[index] + 1)
+                        .asMmolL
+
+                    AreaMark(
+                        x: .value("Time", xValue <= endMarker ? xValue : endMarker),
+                        yStart: .value("Min Value", units == .mgdL ? yMinValue : yMinValue.asMmolL),
+                        yEnd: .value("Max Value", units == .mgdL ? yMaxValue : yMaxValue.asMmolL)
+                    )
+                    .foregroundStyle(Color.blue.opacity(0.5))
+                    .interpolationMethod(.catmullRom)
+
+                } else {
+                    let yMinValue = Decimal(state.minForecast[index]) <= 300 ? Decimal(state.minForecast[index]) : Decimal(300)
+                    let yMaxValue = Decimal(state.maxForecast[index]) <= 300 ? Decimal(state.maxForecast[index]) : Decimal(300)
+
+                    AreaMark(
+                        x: .value("Time", timeForIndex(Int32(index)) <= endMarker ? timeForIndex(Int32(index)) : endMarker),
+                        yStart: .value("Min Value", units == .mgdL ? yMinValue : yMinValue.asMmolL),
+                        yEnd: .value("Max Value", units == .mgdL ? yMaxValue : yMaxValue.asMmolL)
+                    )
+                    .foregroundStyle(Color.blue.opacity(0.5))
+                    .interpolationMethod(.catmullRom)
+                }
+            }
+        }
+    }
+
+    private func drawForecastLines() -> some ChartContent {
+        let predictions = state.predictionsForChart
+
+        // Prepare the prediction data with only the first 36 values, i.e. 3 hours in the future
+        let predictionData = [
+            ("iob", predictions?.iob?.prefix(36)),
+            ("zt", predictions?.zt?.prefix(36)),
+            ("cob", predictions?.cob?.prefix(36)),
+            ("uam", predictions?.uam?.prefix(36))
+        ]
+
+        return ForEach(predictionData, id: \.0) { name, values in
+            if let values = values {
+                ForEach(values.indices, id: \.self) { index in
+                    LineMark(
+                        x: .value("Time", timeForIndex(Int32(index))),
+                        y: .value("Value", units == .mgdL ? Decimal(values[index]) : Decimal(values[index]).asMmolL)
+                    )
+                    .foregroundStyle(by: .value("Prediction Type", name))
+                }
+            }
+        }
+    }
+
+    private func drawCurrentTimeMarker() -> some ChartContent {
+        RuleMark(
+            x: .value(
+                "",
+                Date(timeIntervalSince1970: TimeInterval(NSDate().timeIntervalSince1970)),
+                unit: .second
+            )
+        ).lineStyle(.init(lineWidth: 2, dash: [3])).foregroundStyle(Color(.systemGray2))
+    }
+
+    private var forecastChartXAxis: some AxisContent {
+        AxisMarks(values: .stride(by: .hour, count: 2)) { _ in
+            AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
+            AxisValueLabel(format: .dateTime.hour(.defaultDigits(amPM: .narrow)), anchor: .top)
+                .font(.footnote)
+                .foregroundStyle(Color.primary)
+        }
+    }
+
+    private var forecastChartYAxis: some AxisContent {
+        AxisMarks(position: .trailing) { _ in
+            AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
+            AxisTick(length: 3, stroke: .init(lineWidth: 3)).foregroundStyle(Color.secondary)
+            AxisValueLabel().font(.footnote).foregroundStyle(Color.primary)
+        }
+    }
+}

+ 333 - 0
FreeAPS/Sources/Modules/Bolus/View/MealPresetView.swift

@@ -0,0 +1,333 @@
+import CoreData
+import Foundation
+import SwiftUI
+
+struct MealPresetView: View {
+    @StateObject var state: Bolus.StateModel
+    @Environment(\.colorScheme) var colorScheme
+    @Environment(\.dismiss) var dismiss
+    @Environment(\.managedObjectContext) var moc
+
+    @State private var showAlert = false
+    @State private var dish: String = ""
+    @State private var showAddNewPresetSheet = false
+
+    @State private var presetCarbs: Decimal = 0
+    @State private var presetFat: Decimal = 0
+    @State private var presetProtein: Decimal = 0
+
+    @State private var carbs: Decimal = 0
+    @State private var fat: Decimal = 0
+    @State private var protein: Decimal = 0
+
+    @FetchRequest(
+        entity: MealPresetStored.entity(),
+        sortDescriptors: [NSSortDescriptor(key: "dish", ascending: true)]
+    ) var carbPresets: FetchedResults<MealPresetStored>
+
+    private var mealFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 1
+        return formatter
+    }
+
+    private var color: LinearGradient {
+        colorScheme == .dark ? LinearGradient(
+            gradient: Gradient(colors: [
+                Color.bgDarkBlue,
+                Color.bgDarkerDarkBlue
+            ]),
+            startPoint: .top,
+            endPoint: .bottom
+        )
+            :
+            LinearGradient(
+                gradient: Gradient(colors: [Color.gray.opacity(0.1)]),
+                startPoint: .top,
+                endPoint: .bottom
+            )
+    }
+
+    var body: some View {
+        NavigationStack {
+            Form {
+                mealPresets
+                dishInfos()
+                addPresetToTreatmentsButton
+            }
+            .scrollContentBackground(.hidden).background(color)
+            .navigationTitle("Meal Presets")
+            .navigationBarTitleDisplayMode(.automatic)
+            .toolbar(content: {
+                ToolbarItem(placement: .topBarLeading) {
+                    Button {
+                        dismiss()
+                        resetValues()
+                    } label: {
+                        Text("Close")
+                    }
+                }
+                ToolbarItem(placement: .topBarTrailing) {
+                    Button(action: {
+                        showAddNewPresetSheet.toggle()
+                        resetValues()
+                    }, label: {
+                        HStack {
+                            Text("New Preset")
+                            Image(systemName: "plus")
+                        }
+                    })
+                }
+            })
+            .sheet(isPresented: $showAddNewPresetSheet) {
+                AddMealPresetView(
+                    dish: $dish,
+                    presetCarbs: $presetCarbs,
+                    presetFat: $presetFat,
+                    presetProtein: $presetProtein,
+                    onSave: savePreset,
+                    onCancel: {
+                        showAddNewPresetSheet.toggle()
+                        resetValues()
+                    }
+                )
+            }
+            .onDisappear {
+                resetValues()
+            }
+        }
+    }
+
+    private var mealPresets: some View {
+        Section {
+            HStack {
+                if state.selection != nil {
+                    minusButton
+                }
+                Picker("Preset", selection: $state.selection) {
+                    Text("Saved Food").tag(nil as MealPresetStored?)
+                    ForEach(carbPresets, id: \.self) { (preset: MealPresetStored) in
+                        Text(preset.dish ?? "").tag(preset as MealPresetStored?)
+                    }
+                }
+                .labelsHidden()
+                .frame(maxWidth: .infinity, alignment: .center)
+                if state.selection != nil {
+                    plusButton
+                }
+            }
+
+            HStack {
+                Spacer()
+
+                Button("Delete Preset") {
+                    showAlert.toggle()
+                }
+                .disabled(state.selection == nil)
+                .tint(.orange)
+                .buttonStyle(.borderless)
+                .alert(
+                    "Delete preset '\(state.selection?.dish ?? "")'?",
+                    isPresented: $showAlert,
+                    actions: {
+                        Button("No", role: .cancel) {}
+                        Button("Yes", role: .destructive) {
+                            if let selection = state.selection {
+                                let previousSelection = state.selection
+                                let count = state.summation.filter { $0 == selection.dish }.count
+                                state.summation.removeAll { $0 == selection.dish }
+                                carbs -= (((selection.carbs ?? 0) as NSDecimalNumber) as Decimal) * Decimal(count)
+                                fat -= (((selection.fat ?? 0) as NSDecimalNumber) as Decimal) * Decimal(count)
+                                protein -= (((selection.protein ?? 0) as NSDecimalNumber) as Decimal) * Decimal(count)
+                                state.deletePreset()
+                                state.selection = previousSelection
+                            }
+                        }
+                    }
+                )
+
+                Spacer()
+            }
+        }.listRowBackground(Color.chart)
+    }
+
+    private var addPresetToTreatmentsButton: some View {
+        Button {
+            state.carbs += carbs
+            state.fat += fat
+            state.protein += protein
+
+            dismiss()
+        }
+        label: {
+            Text("Add to treatments")
+                .font(.headline)
+                .foregroundStyle(Color.white)
+                .frame(maxWidth: .infinity, alignment: .center)
+        }
+        .disabled(noPresetChosen)
+        .listRowBackground(noPresetChosen ? Color(.systemGray3) : Color(.systemBlue))
+        .shadow(radius: 3)
+        .clipShape(RoundedRectangle(cornerRadius: 8))
+    }
+
+    private var noPresetChosen: Bool {
+        state.selection == nil || carbs == 0 || fat == 0 || protein == 0
+    }
+
+    @ViewBuilder private func dishInfos() -> some View {
+        if !state.summation.isEmpty {
+            let presetSummary = generatePresetSummary()
+
+            Section(header: Text("Summary")) {
+                presetSummary
+                    .lineLimit(nil) // In case the text is too long, allow it to wrap to the next line
+
+                LazyVGrid(columns: [
+                    GridItem(.flexible(), alignment: .leading),
+                    GridItem(.flexible(), alignment: .trailing)
+                ], spacing: 0) {
+                    Group {
+                        Text("Carbs: ")
+                            .font(.footnote)
+                            .foregroundStyle(.secondary)
+                        HStack(spacing: 2) {
+                            Text("\(carbs as NSNumber, formatter: mealFormatter)")
+                                .font(.footnote)
+                            Text(" g")
+                                .font(.footnote)
+                                .foregroundStyle(.secondary)
+                        }
+                    }
+
+                    Group {
+                        Text("Fat: ")
+                            .font(.footnote)
+                            .foregroundStyle(.secondary)
+                        HStack(spacing: 2) {
+                            Text("\(fat as NSNumber, formatter: mealFormatter)")
+                                .font(.footnote)
+                            Text(" g")
+                                .font(.footnote)
+                                .foregroundStyle(.secondary)
+                        }
+                    }
+
+                    Group {
+                        Text("Protein: ")
+                            .font(.footnote)
+                            .foregroundStyle(.secondary)
+                        HStack(spacing: 2) {
+                            Text("\(protein as NSNumber, formatter: mealFormatter)")
+                                .font(.footnote)
+                            Text(" g")
+                                .font(.footnote)
+                                .foregroundStyle(.secondary)
+                        }
+                    }
+                }
+            }.listRowBackground(Color.chart)
+        }
+    }
+
+    private func generatePresetSummary() -> some View {
+        var counts = [String: Int]()
+
+        for preset in state.summation {
+            counts[preset, default: 0] += 1
+        }
+
+        return VStack(alignment: .leading) {
+            ForEach(counts.sorted(by: { $0.key < $1.key }), id: \.key) { key, value in
+                if value > 0 {
+                    HStack {
+                        Text("\(value) x")
+                            .foregroundColor(.blue)
+                        Text(key)
+                    }
+                }
+            }
+        }
+    }
+
+    private func resetValues() {
+        dish = ""
+        presetCarbs = 0
+        presetFat = 0
+        presetProtein = 0
+        state.selection = nil
+        state.summation.removeAll()
+    }
+
+    private var minusButton: some View {
+        Button {
+            if carbs != 0 {
+                carbs -= (((state.selection?.carbs ?? 0) as NSDecimalNumber) as Decimal)
+            } else { carbs = 0 }
+
+            if fat != 0,
+               (fat - (((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal) as Decimal) >= 0
+            {
+                fat -= (((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal)
+            } else { fat = 0 }
+
+            if protein != 0,
+               (protein - (((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal) as Decimal) >= 0
+            {
+                protein -= (((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal)
+            } else { protein = 0 }
+
+            state.removePresetFromNewMeal()
+            if carbs == 0, fat == 0, protein == 0 { state.summation = [] }
+        }
+        label: { Image(systemName: "minus.circle.fill")
+            .font(.system(size: 20))
+        }
+        .disabled(
+            state
+                .selection == nil ||
+                (
+                    !state.summation
+                        .contains(state.selection?.dish ?? "") && (state.selection?.dish ?? "") != ""
+                )
+        )
+        .buttonStyle(.borderless)
+        .tint(.blue)
+    }
+
+    private var plusButton: some View {
+        Button {
+            carbs += ((state.selection?.carbs ?? 0) as NSDecimalNumber) as Decimal
+            fat += ((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal
+            protein += ((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal
+
+            state.addPresetToNewMeal()
+        }
+        label: { Image(systemName: "plus.circle.fill")
+            .font(.system(size: 20))
+        }
+        .disabled(state.selection == nil)
+        .buttonStyle(.borderless)
+        .tint(.blue)
+    }
+
+    private func savePreset() {
+        if dish != "" {
+            let preset = MealPresetStored(context: moc)
+            preset.dish = dish
+            preset.fat = presetFat as NSDecimalNumber
+            preset.protein = presetProtein as NSDecimalNumber
+            preset.carbs = presetCarbs as NSDecimalNumber
+
+            do {
+                guard moc.hasChanges else { return }
+                try moc.save()
+                showAddNewPresetSheet.toggle()
+                resetValues()
+            } catch let error as NSError {
+                debugPrint("\(DebuggingIdentifiers.failed) Failed to save Meal Preset with error: \(error.userInfo)")
+            }
+        }
+    }
+}

+ 5 - 0
FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -8,6 +8,7 @@ extension DataTable {
         @Injected() var unlockmanager: UnlockManager!
         @Injected() private var storage: FileStorage!
         @Injected() var pumpHistoryStorage: PumpHistoryStorage!
+        @Injected() var glucoseStorage: GlucoseStorage!
         @Injected() var healthKitManager: HealthKitManager!
 
         let coredataContext = CoreDataStack.shared.newTaskContext()
@@ -31,6 +32,10 @@ extension DataTable {
             broadcaster.register(DeterminationObserver.self, observer: self)
         }
 
+        func isGlucoseDataFresh(_ glucoseDate: Date?) -> Bool {
+            glucoseStorage.isGlucoseDataFresh(glucoseDate)
+        }
+
         // Carb and FPU deletion from history
         /// marked as MainActor to be able to publish changes from the background
         /// - Parameter: NSManagedObjectID to be able to transfer the object safely from one thread to another thread

+ 30 - 20
FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift

@@ -12,13 +12,11 @@ extension DataTable {
         @State private var alertTreatmentToDelete: PumpEventStored?
         @State private var alertCarbEntryToDelete: CarbEntryStored?
         @State private var alertGlucoseToDelete: GlucoseStored?
-
+        @State private var showAlert = false
         @State private var showFutureEntries: Bool = false // default to hide future entries
         @State private var showManualGlucose: Bool = false
         @State private var isAmountUnconfirmed: Bool = true
 
-        @State private var showAlert = false
-
         @Environment(\.colorScheme) var colorScheme
         @Environment(\.managedObjectContext) var context
 
@@ -130,7 +128,9 @@ extension DataTable {
                         .background(color)
                 }.blur(radius: state.waitForSuggestion ? 8 : 0)
 
-                if state.waitForSuggestion {
+                // Show custom progress view
+                /// don't show it if glucose is stale as it will block the UI
+                if state.waitForSuggestion && state.isGlucoseDataFresh(glucoseStored.first?.date) {
                     CustomProgressView(text: progressText.rawValue)
                 }
             })
@@ -343,6 +343,7 @@ extension DataTable {
                                     state.addManualGlucose()
                                     isAmountUnconfirmed = false
                                     showManualGlucose = false
+                                    state.mode = .glucose
                                 }
                                 label: { Text("Save") }
                                     .frame(maxWidth: .infinity, alignment: .center)
@@ -451,24 +452,33 @@ extension DataTable {
         }
 
         @ViewBuilder private func mealView(_ meal: CarbEntryStored) -> some View {
-            HStack {
-                if meal.isFPU {
-                    Image(systemName: "circle.fill").foregroundColor(Color.orange.opacity(0.5))
-                    Text("Fat / Protein")
-                    Text((numberFormatter.string(for: meal.carbs) ?? "0") + NSLocalizedString(" g", comment: "gram of carbs"))
-                } else {
-                    Image(systemName: "circle.fill").foregroundColor(Color.loopYellow)
-                    Text("Carbs")
-                    Text(
-                        (numberFormatter.string(for: meal.carbs) ?? "0") +
-                            NSLocalizedString(" g", comment: "gram of carb equilvalents")
-                    )
-                }
+            VStack {
+                HStack {
+                    if meal.isFPU {
+                        Image(systemName: "circle.fill").foregroundColor(Color.orange.opacity(0.5))
+                        Text("Fat / Protein")
+                        Text((numberFormatter.string(for: meal.carbs) ?? "0") + NSLocalizedString(" g", comment: "gram of carbs"))
+                    } else {
+                        Image(systemName: "circle.fill").foregroundColor(Color.loopYellow)
+                        Text("Carbs")
+                        Text(
+                            (numberFormatter.string(for: meal.carbs) ?? "0") +
+                                NSLocalizedString(" g", comment: "gram of carb equilvalents")
+                        )
+                    }
 
-                Spacer()
+                    Spacer()
 
-                Text(dateFormatter.string(from: meal.date ?? Date()))
-                    .moveDisabled(true)
+                    Text(dateFormatter.string(from: meal.date ?? Date()))
+                        .moveDisabled(true)
+                }
+                if let note = meal.note, note != "" {
+                    HStack {
+                        Image(systemName: "square.and.pencil")
+                        Text(note)
+                        Spacer()
+                    }.padding(.top, 5).foregroundColor(.secondary)
+                }
             }
             .swipeActions {
                 Button(

+ 8 - 0
FreeAPS/Sources/Modules/HealthKit/View/AppleHealthKitRootView.swift

@@ -27,6 +27,14 @@ extension AppleHealthKit {
         var body: some View {
             Form {
                 Section {
+                    HStack {
+                        Image(systemName: "exclamationmark.triangle")
+                        Text(
+                            "Connecting to Apple Health will use an excessive amount of storage and may cause Apple Health to lag. This will be improved in a future release."
+                        )
+                        .font(.caption)
+                    }
+                    .foregroundColor(Color.secondary)
                     Toggle("Connect to Apple Health", isOn: $state.useAppleHealth)
                     HStack {
                         Image(systemName: "pencil.circle.fill")

+ 216 - 170
FreeAPS/Sources/Modules/Home/HomeStateModel.swift

@@ -12,6 +12,7 @@ extension Home {
         @Injected() var fetchGlucoseManager: FetchGlucoseManager!
         @Injected() var nightscoutManager: NightscoutManager!
         @Injected() var determinationStorage: DeterminationStorage!
+        @Injected() var glucoseStorage: GlucoseStorage!
         private let timer = DispatchTimer(timeInterval: 5)
         private(set) var filteredHours = 24
         @Published var manualGlucose: [BloodGlucose] = []
@@ -46,8 +47,8 @@ extension Home {
         @Published var manualTempBasal = false
         @Published var smooth = false
         @Published var maxValue: Decimal = 1.2
-        @Published var lowGlucose: Decimal = 4 / 0.0555
-        @Published var highGlucose: Decimal = 10 / 0.0555
+        @Published var lowGlucose: Decimal = 70
+        @Published var highGlucose: Decimal = 180
         @Published var overrideUnit: Bool = false
         @Published var dynamicBGColor: Bool = false
         @Published var displayXgridLines: Bool = false
@@ -82,6 +83,11 @@ extension Home {
         @Published var pumpStatusHighlightMessage: String? = nil
         @Published var cgmAvailable: Bool = false
 
+        @Published var minForecast: [Int] = []
+        @Published var maxForecast: [Int] = []
+        @Published var minCount: Int = 12 // count of Forecasts drawn in 5 min distances, i.e. 12 means a min of 1 hour
+        @Published var displayForecastsAsLines: Bool = false
+
         let context = CoreDataStack.shared.newTaskContext()
         let viewContext = CoreDataStack.shared.persistentContainer.viewContext
 
@@ -121,8 +127,8 @@ extension Home {
             setupCurrentTempTarget()
             smooth = settingsManager.settings.smoothGlucose
             maxValue = settingsManager.preferences.autosensMax
-            lowGlucose = settingsManager.settings.low
-            highGlucose = settingsManager.settings.high
+            lowGlucose = units == .mgdL ? settingsManager.settings.low : settingsManager.settings.low.asMmolL
+            highGlucose = units == .mgdL ? settingsManager.settings.high : settingsManager.settings.high.asMmolL
             overrideUnit = settingsManager.settings.overrideHbA1cUnit
             dynamicBGColor = settingsManager.settings.dynamicBGColor
             displayXgridLines = settingsManager.settings.xGridLines
@@ -131,6 +137,8 @@ extension Home {
             tins = settingsManager.settings.tins
             cgmAvailable = fetchGlucoseManager.cgmGlucoseSourceType != CGMType.none
 
+            displayForecastsAsLines = settingsManager.settings.displayForecastsAsLines
+
             broadcaster.register(GlucoseObserver.self, observer: self)
             broadcaster.register(DeterminationObserver.self, observer: self)
             broadcaster.register(SettingsObserver.self, observer: self)
@@ -231,10 +239,7 @@ extension Home {
         private func registerHandlers() {
             coreDataObserver?.registerHandler(for: "OrefDetermination") { [weak self] in
                 guard let self = self else { return }
-                Task {
-                    self.setupDeterminationsArray()
-                    await self.updateForecastData()
-                }
+                self.setupDeterminationsArray()
             }
 
             coreDataObserver?.registerHandler(for: "GlucoseStored") { [weak self] in
@@ -291,6 +296,11 @@ extension Home {
             provider.heartbeatNow()
         }
 
+        func showProgressView() {
+            glucoseStorage
+                .isGlucoseDataFresh(glucoseFromPersistence.first?.date) ? (waitForSuggestion = true) : (waitForSuggestion = false)
+        }
+
         func cancelBolus() {
             Task {
                 await apsManager.cancelBolus()
@@ -455,13 +465,14 @@ extension Home.StateModel:
         animatedBackground = settingsManager.settings.animatedBackground
         manualTempBasal = apsManager.isManualTempBasal
         smooth = settingsManager.settings.smoothGlucose
-        lowGlucose = settingsManager.settings.low
-        highGlucose = settingsManager.settings.high
+        lowGlucose = units == .mgdL ? settingsManager.settings.low : settingsManager.settings.low.asMmolL
+        highGlucose = units == .mgdL ? settingsManager.settings.high : settingsManager.settings.high.asMmolL
         overrideUnit = settingsManager.settings.overrideHbA1cUnit
         dynamicBGColor = settingsManager.settings.dynamicBGColor
         displayXgridLines = settingsManager.settings.xGridLines
         displayYgridLines = settingsManager.settings.yGridLines
         thresholdLines = settingsManager.settings.rulerMarks
+        displayForecastsAsLines = settingsManager.settings.displayForecastsAsLines
         tins = settingsManager.settings.tins
         cgmAvailable = (fetchGlucoseManager.cgmGlucoseSourceType != CGMType.none)
         displayPumpStatusHighlightMessage()
@@ -563,7 +574,8 @@ extension Home.StateModel {
     private func setupGlucoseArray() {
         Task {
             let ids = await self.fetchGlucose()
-            await updateGlucoseArray(with: ids)
+            let glucoseObjects: [GlucoseStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
+            await updateGlucoseArray(with: glucoseObjects)
         }
     }
 
@@ -577,29 +589,24 @@ extension Home.StateModel {
             fetchLimit: 288
         )
 
+        guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+
         return await context.perform {
-            return results.map(\.objectID)
+            return fetchedResults.map(\.objectID)
         }
     }
 
-    @MainActor private func updateGlucoseArray(with IDs: [NSManagedObjectID]) {
-        do {
-            let glucoseObjects = try IDs.compactMap { id in
-                try viewContext.existingObject(with: id) as? GlucoseStored
-            }
-            glucoseFromPersistence = glucoseObjects
-        } catch {
-            debugPrint(
-                "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the glucose array: \(error.localizedDescription)"
-            )
-        }
+    @MainActor private func updateGlucoseArray(with objects: [GlucoseStored]) {
+        glucoseFromPersistence = objects
     }
 
     // Setup Manual Glucose
     private func setupManualGlucoseArray() {
         Task {
             let ids = await self.fetchManualGlucose()
-            await updateManualGlucoseArray(with: ids)
+            let manualGlucoseObjects: [GlucoseStored] = await CoreDataStack.shared
+                .getNSManagedObject(with: ids, context: viewContext)
+            await updateManualGlucoseArray(with: manualGlucoseObjects)
         }
     }
 
@@ -613,29 +620,23 @@ extension Home.StateModel {
             fetchLimit: 288
         )
 
+        guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+
         return await context.perform {
-            return results.map(\.objectID)
+            return fetchedResults.map(\.objectID)
         }
     }
 
-    @MainActor private func updateManualGlucoseArray(with IDs: [NSManagedObjectID]) {
-        do {
-            let manualGlucoseObjects = try IDs.compactMap { id in
-                try viewContext.existingObject(with: id) as? GlucoseStored
-            }
-            manualGlucoseFromPersistence = manualGlucoseObjects
-        } catch {
-            debugPrint(
-                "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the manual glucose array: \(error.localizedDescription)"
-            )
-        }
+    @MainActor private func updateManualGlucoseArray(with objects: [GlucoseStored]) {
+        manualGlucoseFromPersistence = objects
     }
 
     // Setup Carbs
     private func setupCarbsArray() {
         Task {
             let ids = await self.fetchCarbs()
-            await updateCarbsArray(with: ids)
+            let carbObjects: [CarbEntryStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
+            await updateCarbsArray(with: carbObjects)
         }
     }
 
@@ -648,29 +649,23 @@ extension Home.StateModel {
             ascending: false
         )
 
+        guard let fetchedResults = results as? [CarbEntryStored] else { return [] }
+
         return await context.perform {
-            return results.map(\.objectID)
+            return fetchedResults.map(\.objectID)
         }
     }
 
-    @MainActor private func updateCarbsArray(with IDs: [NSManagedObjectID]) {
-        do {
-            let carbObjects = try IDs.compactMap { id in
-                try viewContext.existingObject(with: id) as? CarbEntryStored
-            }
-            carbsFromPersistence = carbObjects
-        } catch {
-            debugPrint(
-                "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the carbs array: \(error.localizedDescription)"
-            )
-        }
+    @MainActor private func updateCarbsArray(with objects: [CarbEntryStored]) {
+        carbsFromPersistence = objects
     }
 
     // Setup FPUs
     private func setupFPUsArray() {
         Task {
             let ids = await self.fetchFPUs()
-            await updateFPUsArray(with: ids)
+            let fpuObjects: [CarbEntryStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
+            await updateFPUsArray(with: fpuObjects)
         }
     }
 
@@ -683,22 +678,15 @@ extension Home.StateModel {
             ascending: false
         )
 
+        guard let fetchedResults = results as? [CarbEntryStored] else { return [] }
+
         return await context.perform {
-            return results.map(\.objectID)
+            return fetchedResults.map(\.objectID)
         }
     }
 
-    @MainActor private func updateFPUsArray(with IDs: [NSManagedObjectID]) {
-        do {
-            let fpuObjects = try IDs.compactMap { id in
-                try viewContext.existingObject(with: id) as? CarbEntryStored
-            }
-            fpusFromPersistence = fpuObjects
-        } catch {
-            debugPrint(
-                "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the fpus array: \(error.localizedDescription)"
-            )
-        }
+    @MainActor private func updateFPUsArray(with objects: [CarbEntryStored]) {
+        fpusFromPersistence = objects
     }
 
     // Custom fetch to more efficiently filter only for cob and iob
@@ -710,17 +698,22 @@ extension Home.StateModel {
             key: "deliverAt",
             ascending: false,
             batchSize: 50,
-            propertiesToFetch: ["cob", "iob", "deliverAt"]
+            propertiesToFetch: ["cob", "iob", "objectID"]
         )
 
+        guard let fetchedResults = results as? [[String: Any]] else {
+            return []
+        }
+
         return await context.perform {
-            return results.map(\.objectID)
+            return fetchedResults.compactMap { $0["objectID"] as? NSManagedObjectID }
         }
     }
 
     // Setup Determinations
     private func setupDeterminationsArray() {
         Task {
+            // Get the NSManagedObjectIDs
             async let enactedObjectIDs = determinationStorage
                 .fetchLastDeterminationObjectID(predicate: NSPredicate.enactedDetermination)
             async let enactedAndNonEnactedObjectIDs = fetchCobAndIob()
@@ -728,14 +721,10 @@ extension Home.StateModel {
             let enactedIDs = await enactedObjectIDs
             let enactedAndNonEnactedIDs = await enactedAndNonEnactedObjectIDs
 
-            async let updateEnacted: () = updateDeterminationsArray(with: enactedIDs, keyPath: \.determinationsFromPersistence)
-            async let updateEnactedAndNonEnacted: () = updateDeterminationsArray(
-                with: enactedAndNonEnactedIDs,
-                keyPath: \.enactedAndNonEnactedDeterminations
-            )
+            // Get the NSManagedObjects and return them on the Main Thread
+            await updateDeterminationsArray(with: enactedIDs, keyPath: \.determinationsFromPersistence)
+            await updateDeterminationsArray(with: enactedAndNonEnactedIDs, keyPath: \.enactedAndNonEnactedDeterminations)
 
-            await updateEnacted
-            await updateEnactedAndNonEnacted
             await updateForecastData()
         }
     }
@@ -743,24 +732,21 @@ extension Home.StateModel {
     @MainActor private func updateDeterminationsArray(
         with IDs: [NSManagedObjectID],
         keyPath: ReferenceWritableKeyPath<Home.StateModel, [OrefDetermination]>
-    ) {
-        do {
-            let determinationObjects = try IDs.compactMap { id in
-                try viewContext.existingObject(with: id) as? OrefDetermination
-            }
-            self[keyPath: keyPath] = determinationObjects
-        } catch {
-            debugPrint(
-                "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the determinations array: \(error.localizedDescription)"
-            )
-        }
+    ) async {
+        // Fetch the objects off the main thread
+        let determinationObjects: [OrefDetermination] = await CoreDataStack.shared
+            .getNSManagedObject(with: IDs, context: viewContext)
+
+        // Update the array on the main thread
+        self[keyPath: keyPath] = determinationObjects
     }
 
     // Setup Insulin
     private func setupInsulinArray() {
         Task {
             let ids = await self.fetchInsulin()
-            await updateInsulinArray(with: ids)
+            let insulinObjects: [PumpEventStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
+            await updateInsulinArray(with: insulinObjects)
         }
     }
 
@@ -773,36 +759,30 @@ extension Home.StateModel {
             ascending: true
         )
 
+        guard let pumpEvents = results as? [PumpEventStored] else {
+            return []
+        }
+
         return await context.perform {
-            return results.map(\.objectID)
+            return pumpEvents.map(\.objectID)
         }
     }
 
-    @MainActor private func updateInsulinArray(with IDs: [NSManagedObjectID]) {
-        do {
-            let insulinObjects = try IDs.compactMap { id in
-                try viewContext.existingObject(with: id) as? PumpEventStored
-            }
-            insulinFromPersistence = insulinObjects
-
-            // filter tempbasals
-            manualTempBasal = apsManager.isManualTempBasal
-            tempBasals = insulinFromPersistence.filter({ $0.tempBasal != nil })
-
-            // suspension and resume events
-            suspensions = insulinFromPersistence
-                .filter({ $0.type == EventType.pumpSuspend.rawValue || $0.type == EventType.pumpResume.rawValue })
-            let lastSuspension = suspensions.last
+    @MainActor private func updateInsulinArray(with insulinObjects: [PumpEventStored]) {
+        insulinFromPersistence = insulinObjects
 
-            pumpSuspended = tempBasals.last?.timestamp ?? Date() > lastSuspension?.timestamp ?? .distantPast && lastSuspension?
-                .type == EventType.pumpSuspend
-                .rawValue
+        // Filter tempbasals
+        manualTempBasal = apsManager.isManualTempBasal
+        tempBasals = insulinFromPersistence.filter({ $0.tempBasal != nil })
 
-        } catch {
-            debugPrint(
-                "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the insulin array: \(error.localizedDescription)"
-            )
+        // Suspension and resume events
+        suspensions = insulinFromPersistence.filter {
+            $0.type == EventType.pumpSuspend.rawValue || $0.type == EventType.pumpResume.rawValue
         }
+        let lastSuspension = suspensions.last
+
+        pumpSuspended = tempBasals.last?.timestamp ?? Date() > lastSuspension?.timestamp ?? .distantPast && lastSuspension?
+            .type == EventType.pumpSuspend.rawValue
     }
 
     // Setup Last Bolus to display the bolus progress bar
@@ -824,8 +804,10 @@ extension Home.StateModel {
             fetchLimit: 1
         )
 
+        guard let fetchedResults = results as? [PumpEventStored] else { return [].first }
+
         return await context.perform {
-            return results.map(\.objectID).first
+            return fetchedResults.map(\.objectID).first
         }
     }
 
@@ -843,7 +825,8 @@ extension Home.StateModel {
     private func setupBatteryArray() {
         Task {
             let ids = await self.fetchBattery()
-            await updateBatteryArray(with: ids)
+            let batteryObjects: [OpenAPS_Battery] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
+            await updateBatteryArray(with: batteryObjects)
         }
     }
 
@@ -856,22 +839,15 @@ extension Home.StateModel {
             ascending: false
         )
 
+        guard let fetchedResults = results as? [OpenAPS_Battery] else { return [] }
+
         return await context.perform {
-            return results.map(\.objectID)
+            return fetchedResults.map(\.objectID)
         }
     }
 
-    @MainActor private func updateBatteryArray(with IDs: [NSManagedObjectID]) {
-        do {
-            let batteryObjects = try IDs.compactMap { id in
-                try viewContext.existingObject(with: id) as? OpenAPS_Battery
-            }
-            batteryFromPersistence = batteryObjects
-        } catch {
-            debugPrint(
-                "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the battery array: \(error.localizedDescription)"
-            )
-        }
+    @MainActor private func updateBatteryArray(with objects: [OpenAPS_Battery]) {
+        batteryFromPersistence = objects
     }
 }
 
@@ -880,7 +856,8 @@ extension Home.StateModel {
     private func setupOverrides() {
         Task {
             let ids = await self.fetchOverrides()
-            await updateOverrideArray(with: ids)
+            let overrideObjects: [OverrideStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
+            await updateOverrideArray(with: overrideObjects)
         }
     }
 
@@ -893,23 +870,15 @@ extension Home.StateModel {
             ascending: false
         )
 
+        guard let fetchedResults = results as? [OverrideStored] else { return [] }
+
         return await context.perform {
-            return results.map(\.objectID)
+            return fetchedResults.map(\.objectID)
         }
     }
 
-    @MainActor private func updateOverrideArray(with IDs: [NSManagedObjectID]) {
-        do {
-            let overrideObjects = try IDs.compactMap { id in
-                try viewContext.existingObject(with: id) as? OverrideStored
-            }
-
-            overrides = overrideObjects
-        } catch {
-            debugPrint(
-                "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the override array: \(error.localizedDescription)"
-            )
-        }
+    @MainActor private func updateOverrideArray(with objects: [OverrideStored]) {
+        overrides = objects
     }
 
     @MainActor func calculateDuration(override: OverrideStored) -> TimeInterval {
@@ -930,7 +899,9 @@ extension Home.StateModel {
     private func setupOverrideRunStored() {
         Task {
             let ids = await self.fetchOverrideRunStored()
-            await updateOverrideRunStoredArray(with: ids)
+            let overrideRunObjects: [OverrideRunStored] = await CoreDataStack.shared
+                .getNSManagedObject(with: ids, context: viewContext)
+            await updateOverrideRunStoredArray(with: overrideRunObjects)
         }
     }
 
@@ -944,24 +915,15 @@ extension Home.StateModel {
             ascending: false
         )
 
+        guard let fetchedResults = results as? [OverrideRunStored] else { return [] }
+
         return await context.perform {
-            return results.map(\.objectID)
+            return fetchedResults.map(\.objectID)
         }
     }
 
-    @MainActor private func updateOverrideRunStoredArray(with IDs: [NSManagedObjectID]) {
-        do {
-            let overrideObjects = try IDs.compactMap { id in
-                try viewContext.existingObject(with: id) as? OverrideRunStored
-            }
-
-            overrideRunStored = overrideObjects
-            debugPrint("expiredOverrides: \(DebuggingIdentifiers.inProgress) \(overrideRunStored)")
-        } catch {
-            debugPrint(
-                "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the Override Run Stored array: \(error.localizedDescription)"
-            )
-        }
+    @MainActor private func updateOverrideRunStoredArray(with objects: [OverrideRunStored]) {
+        overrideRunStored = objects
     }
 
     @MainActor func saveToOverrideRunStored(withID id: NSManagedObjectID) async {
@@ -985,41 +947,125 @@ extension Home.StateModel {
     }
 }
 
-// MARK: Extension for Main Chart to draw Forecasts
-
 extension Home.StateModel {
+    // Asynchronously preprocess forecast data in a background thread
     func preprocessForecastData() async -> [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])] {
-        guard let id = determinationsFromPersistence.first?.objectID else {
-            return []
-        }
+        await Task.detached { [self] () -> [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])] in
+            // Get the first determination ID from persistence
+            guard let id = enactedAndNonEnactedDeterminations.first?.objectID else {
+                return []
+            }
+
+            // Get the forecast IDs for the determination ID
+            let forecastIDs = await determinationStorage.getForecastIDs(for: id, in: context)
+            var result: [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])] = []
+
+            // Use a task group to fetch forecast value IDs concurrently
+            await withTaskGroup(of: (UUID, NSManagedObjectID, [NSManagedObjectID]).self) { group in
+                for forecastID in forecastIDs {
+                    group.addTask {
+                        let forecastValueIDs = await self.determinationStorage.getForecastValueIDs(
+                            for: forecastID,
+                            in: self.context
+                        )
+                        return (UUID(), forecastID, forecastValueIDs)
+                    }
+                }
+
+                // Collect the results from the task group
+                for await (uuid, forecastID, forecastValueIDs) in group {
+                    result.append((id: uuid, forecastID: forecastID, forecastValueIDs: forecastValueIDs))
+                }
+            }
+
+            return result
+        }.value
+    }
 
-        // Get forecast and forecast values
-        let forecastIDs = await determinationStorage.getForecastIDs(for: id, in: context)
-        var result: [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])] = []
+    // Fetch forecast values for a given data set
+    func fetchForecastValues(
+        for data: (id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID]),
+        in context: NSManagedObjectContext
+    ) async -> (UUID, Forecast?, [ForecastValue]) {
+        var forecast: Forecast?
+        var forecastValues: [ForecastValue] = []
 
-        for forecastID in forecastIDs {
-            // Get the forecast value IDs for the given forecast ID
-            let forecastValueIDs = await determinationStorage.getForecastValueIDs(for: forecastID, in: context)
-            let uuid = UUID()
-            result.append((id: uuid, forecastID: forecastID, forecastValueIDs: forecastValueIDs))
+        do {
+            try await context.perform {
+                // Fetch the forecast object
+                forecast = try context.existingObject(with: data.forecastID) as? Forecast
+
+                // Fetch the first 3h of forecast values
+                for forecastValueID in data.forecastValueIDs.prefix(36) {
+                    if let forecastValue = try context.existingObject(with: forecastValueID) as? ForecastValue {
+                        forecastValues.append(forecastValue)
+                    }
+                }
+            }
+        } catch {
+            debugPrint(
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch forecast Values with error: \(error.localizedDescription)"
+            )
         }
 
-        return result
+        return (data.id, forecast, forecastValues)
     }
 
+    // Update forecast data and UI on the main thread
     @MainActor func updateForecastData() async {
+        // Preprocess forecast data on a background thread
         let forecastData = await preprocessForecastData()
 
-        preprocessedData = forecastData.reduce(into: []) { result, data in
-            guard let forecast = try? viewContext.existingObject(with: data.forecastID) as? Forecast else {
-                return
-            }
+        var allForecastValues = [[ForecastValue]]()
+        var preprocessedData = [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)]()
 
-            for forecastValueID in data.forecastValueIDs {
-                if let forecastValue = try? viewContext.existingObject(with: forecastValueID) as? ForecastValue {
-                    result.append((id: data.id, forecast: forecast, forecastValue: forecastValue))
+        // Use a task group to fetch forecast values concurrently
+        await withTaskGroup(of: (UUID, Forecast?, [ForecastValue]).self) { group in
+            for data in forecastData {
+                group.addTask {
+                    await self.fetchForecastValues(for: data, in: self.viewContext)
                 }
             }
+
+            // Collect the results from the task group
+            for await (id, forecast, forecastValues) in group {
+                guard let forecast = forecast, !forecastValues.isEmpty else { continue }
+
+                allForecastValues.append(forecastValues)
+                preprocessedData.append(contentsOf: forecastValues.map { (id: id, forecast: forecast, forecastValue: $0) })
+            }
         }
+
+        self.preprocessedData = preprocessedData
+
+        // Ensure there are forecast values to process
+        guard !allForecastValues.isEmpty else {
+            minForecast = []
+            maxForecast = []
+            return
+        }
+
+        minCount = max(12, allForecastValues.map(\.count).min() ?? 0)
+        guard minCount > 0 else { return }
+
+        // Copy allForecastValues to a local constant for thread safety
+        let localAllForecastValues = allForecastValues
+
+        // Calculate min and max forecast values in a background task
+        let (minResult, maxResult) = await Task.detached {
+            let minForecast = (0 ..< self.minCount).map { index in
+                localAllForecastValues.compactMap { $0.indices.contains(index) ? Int($0[index].value) : nil }.min() ?? 0
+            }
+
+            let maxForecast = (0 ..< self.minCount).map { index in
+                localAllForecastValues.compactMap { $0.indices.contains(index) ? Int($0[index].value) : nil }.max() ?? 0
+            }
+
+            return (minForecast, maxForecast)
+        }.value
+
+        // Update the properties on the main thread
+        minForecast = minResult
+        maxForecast = maxResult
     }
 }

+ 321 - 90
FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift

@@ -59,8 +59,9 @@ struct MainChartView: View {
     @State private var basalProfiles: [BasalProfile] = []
     @State private var chartTempTargets: [ChartTempTarget] = []
     @State private var count: Decimal = 1
-    @State private var startMarker = Date(timeIntervalSince1970: TimeInterval(NSDate().timeIntervalSince1970 - 86400))
-    @State private var endMarker = Date(timeIntervalSince1970: TimeInterval(NSDate().timeIntervalSince1970 + 10800))
+    @State private var startMarker =
+        Date(timeIntervalSinceNow: TimeInterval(hours: -24))
+    @State private var endMarker = Date(timeIntervalSinceNow: TimeInterval(hours: 3))
     @State private var minValue: Decimal = 45
     @State private var maxValue: Decimal = 270
     @State private var selection: Date? = nil
@@ -92,10 +93,6 @@ struct MainChartView: View {
         return formatter
     }
 
-    private var conversionFactor: Decimal {
-        units == .mmolL ? 0.0555 : 1
-    }
-
     private var upperLimit: Decimal {
         units == .mgdL ? 400 : 22.2
     }
@@ -108,10 +105,6 @@ struct MainChartView: View {
         units == .mgdL ? 30 : 1.66
     }
 
-    private var interpolationFactor: Double {
-        Double(state.enactedAndNonEnactedDeterminations.first?.cob ?? 1) * 10
-    }
-
     private var selectedGlucose: GlucoseStored? {
         if let selection = selection {
             let lowerBound = selection.addingTimeInterval(-120)
@@ -122,6 +115,30 @@ struct MainChartView: View {
         }
     }
 
+    private var selectedCOBValue: OrefDetermination? {
+        if let selection = selection {
+            let lowerBound = selection.addingTimeInterval(-120)
+            let upperBound = selection.addingTimeInterval(120)
+            return state.enactedAndNonEnactedDeterminations.first {
+                $0.deliverAt ?? now >= lowerBound && $0.deliverAt ?? now <= upperBound
+            }
+        } else {
+            return nil
+        }
+    }
+
+    private var selectedIOBValue: OrefDetermination? {
+        if let selection = selection {
+            let lowerBound = selection.addingTimeInterval(-120)
+            let upperBound = selection.addingTimeInterval(120)
+            return state.enactedAndNonEnactedDeterminations.first {
+                $0.deliverAt ?? now >= lowerBound && $0.deliverAt ?? now <= upperBound
+            }
+        } else {
+            return nil
+        }
+    }
+
     var body: some View {
         VStack {
             ZStack {
@@ -200,6 +217,29 @@ extension Backport {
             content
         }
     }
+
+    @ViewBuilder func chartForegroundStyleScale(state: any StateModel) -> some View {
+        if (state as? Bolus.StateModel)?.displayForecastsAsLines == true ||
+            (state as? Home.StateModel)?.displayForecastsAsLines == true
+        {
+            let modifiedContent = content
+                .chartForegroundStyleScale([
+                    "iob": .blue,
+                    "uam": Color.uam,
+                    "zt": Color.zt,
+                    "cob": .orange
+                ])
+
+            if state is Home.StateModel {
+                modifiedContent
+                    .chartLegend(.hidden)
+            } else {
+                modifiedContent
+            }
+        } else {
+            content
+        }
+    }
 }
 
 extension MainChartView {
@@ -208,11 +248,22 @@ extension MainChartView {
         Chart {
             /// high and low threshold lines
             if thresholdLines {
-                // Auggie - set the Color.loopYellow and Color.loopRed to use dynamic BG with low value passed in
-                RuleMark(y: .value("High", highGlucose * conversionFactor)).foregroundStyle(Color.loopYellow)
-                    .lineStyle(.init(lineWidth: 1, dash: [5]))
-                RuleMark(y: .value("Low", lowGlucose * conversionFactor)).foregroundStyle(Color.loopRed)
-                    .lineStyle(.init(lineWidth: 1, dash: [5]))
+              let lowColor = setBGColor(
+                bgValue: lowGlucose,
+                highBGColorValue: highGlucose,
+                lowBGColorValue: lowGlucose,
+                dynamicBGColor: dynamicBGColor
+              )
+              let highColor = setBGColor(
+                bgValue: highGlucose,
+                highBGColorValue: highGlucose,
+                lowBGColorValue: lowGlucose,
+                dynamicBGColor: dynamicBGColor
+              )
+              RuleMark(y: .value("High", highGlucose)).foregroundStyle(highColor)
+                  .lineStyle(.init(lineWidth: 1, dash: [5]))
+              RuleMark(y: .value("Low", lowGlucose)).foregroundStyle(lowColor)
+                  .lineStyle(.init(lineWidth: 1, dash: [5]))
             }
         }
         .id("DummyMainChart")
@@ -222,7 +273,7 @@ extension MainChartView {
         .chartXScale(domain: startMarker ... endMarker)
         .chartXAxis(.hidden)
         .chartYAxis { mainChartYAxis }
-        .chartYScale(domain: minValue ... maxValue)
+        .chartYScale(domain: units == .mgdL ? minValue ... maxValue : minValue.asMmolL ... maxValue.asMmolL)
         .chartLegend(.hidden)
     }
 
@@ -264,20 +315,54 @@ extension MainChartView {
                 drawTempTargets()
                 drawActiveOverrides()
                 drawOverrideRunStored()
-                drawForecasts()
                 drawGlucose(dummy: false)
                 drawManualGlucose()
                 drawCarbs()
 
+                if state.displayForecastsAsLines {
+                    drawForecastsLines()
+                } else {
+                    drawForecastsCone()
+                }
+
                 /// show glucose value when hovering over it
-                if let selectedGlucose {
-                    RuleMark(x: .value("Selection", selectedGlucose.date ?? now, unit: .minute))
-                        .foregroundStyle(Color.tabBar)
-                        .offset(yStart: 70)
-                        .lineStyle(.init(lineWidth: 2, dash: [5]))
-                        .annotation(position: .top) {
-                            selectionPopover
-                        }
+                if #available(iOS 17, *) {
+                    if let selectedGlucose {
+                        RuleMark(x: .value("Selection", selectedGlucose.date ?? now, unit: .minute))
+                            .foregroundStyle(Color.tabBar)
+                            .offset(yStart: 70)
+                            .lineStyle(.init(lineWidth: 2))
+                            .annotation(
+                                position: .top,
+                                alignment: .center,
+                                overflowResolution: .init(x: .fit(to: .chart), y: .fit(to: .chart))
+                            ) {
+                                selectionPopover
+                            }
+
+                        PointMark(
+                            x: .value("Time", selectedGlucose.date ?? now, unit: .minute),
+                            y: .value("Value", selectedGlucose.glucose)
+                        )
+                        .zIndex(-1)
+                        .symbolSize(CGSize(width: 15, height: 15))
+                        .foregroundStyle(
+                            Decimal(selectedGlucose.glucose) > highGlucose ? Color.orange
+                                .opacity(0.8) :
+                                (
+                                    Decimal(selectedGlucose.glucose) < lowGlucose ? Color.red.opacity(0.8) : Color.green
+                                        .opacity(0.8)
+                                )
+                        )
+
+                        PointMark(
+                            x: .value("Time", selectedGlucose.date ?? now, unit: .minute),
+                            y: .value("Value", selectedGlucose.glucose)
+                        )
+                        .zIndex(-1)
+                        .symbolSize(CGSize(width: 6, height: 6))
+                        .foregroundStyle(Color.primary)
+                    }
                 }
             }
             .id("MainChart")
@@ -297,38 +382,58 @@ extension MainChartView {
             .chartYAxis { mainChartYAxis }
             .chartYAxis(.hidden)
             .backport.chartXSelection(value: $selection)
-            .chartYScale(domain: minValue ... maxValue)
-            .chartForegroundStyleScale([
-                "zt": Color.zt,
-                "uam": Color.uam,
-                "cob": .orange,
-                "iob": .blue
-            ])
-            .chartLegend(.hidden)
+            .chartYScale(domain: units == .mgdL ? minValue ... maxValue : minValue.asMmolL ... maxValue.asMmolL)
+            .backport.chartForegroundStyleScale(state: state)
         }
     }
 
     // Auggie TODO: dynamic BG color here in the pop-over text
     @ViewBuilder var selectionPopover: some View {
         if let sgv = selectedGlucose?.glucose {
-            let glucoseToShow = Decimal(sgv) * conversionFactor
-            VStack {
-                Text(selectedGlucose?.date?.formatted(.dateTime.hour().minute(.twoDigits)) ?? "")
+            let glucoseToShow = units == .mgdL ? Decimal(sgv) : Decimal(sgv).asMmolL
+            VStack(alignment: .leading) {
                 HStack {
-                    Text(glucoseToShow.formatted(.number.precision(units == .mmolL ? .fractionLength(1) : .fractionLength(0))))
-                        .fontWeight(.bold)
-                        .foregroundStyle(
-                            Decimal(sgv) < lowGlucose ? Color
-                                .red : (Decimal(sgv) > highGlucose ? Color.orange : Color.primary)
-                        )
-                    Text(units.rawValue).foregroundColor(.secondary)
+                    Image(systemName: "clock")
+                    Text(selectedGlucose?.date?.formatted(.dateTime.hour().minute(.twoDigits)) ?? "")
+                        .font(.body).bold()
+                }.font(.body).padding(.bottom, 5)
+
+                HStack {
+                    Text(units == .mgdL ? glucoseToShow.description : Decimal(sgv).formattedAsMmolL)
+                        .bold()
+                        + Text(" \(units.rawValue)")
+                }.foregroundStyle(
+                    glucoseToShow < lowGlucose ? Color
+                        .red : (glucoseToShow > highGlucose ? Color.orange : Color.primary)
+                ).font(.body)
+
+                if let selectedIOBValue, let iob = selectedIOBValue.iob {
+                    HStack {
+                        Image(systemName: "syringe.fill").frame(width: 15)
+                        Text(bolusFormatter.string(from: iob) ?? "")
+                            .bold()
+                            + Text(NSLocalizedString(" U", comment: "Insulin unit"))
+                    }.foregroundStyle(Color.insulin).font(.body)
+                }
+
+                if let selectedCOBValue {
+                    HStack {
+                        Image(systemName: "fork.knife").frame(width: 15)
+                        Text(carbsFormatter.string(from: selectedCOBValue.cob as NSNumber) ?? "")
+                            .bold()
+                            + Text(NSLocalizedString(" g", comment: "gram of carbs"))
+                    }.foregroundStyle(Color.orange).font(.body)
                 }
             }
-            .padding(6)
+            .padding()
             .background {
                 RoundedRectangle(cornerRadius: 4)
-                    .fill(Color.gray.opacity(0.1))
-                    .shadow(color: .blue, radius: 2)
+                    .fill(Color.chart.opacity(0.85))
+                    .shadow(color: Color.secondary, radius: 2)
+                    .overlay(
+                        RoundedRectangle(cornerRadius: 4)
+                            .stroke(Color.secondary, lineWidth: 2)
+                    )
             }
         }
     }
@@ -362,8 +467,7 @@ extension MainChartView {
             .chartXAxis { basalChartXAxis }
             .chartXAxis(.hidden)
             .chartYAxis(.hidden)
-            .rotationEffect(.degrees(180))
-            .scaleEffect(x: -1, y: 1, anchor: .center)
+            .chartPlotStyle { basalChartPlotStyle($0) }
         }
     }
 
@@ -371,10 +475,29 @@ extension MainChartView {
         VStack {
             Chart {
                 drawIOB()
+
+                if #available(iOS 17, *) {
+                    if let selectedIOBValue {
+                        PointMark(
+                            x: .value("Time", selectedIOBValue.deliverAt ?? now, unit: .minute),
+                            y: .value("Value", Int(truncating: selectedIOBValue.iob ?? 0))
+                        )
+                        .symbolSize(CGSize(width: 15, height: 15))
+                        .foregroundStyle(Color.darkerBlue.opacity(0.8))
+
+                        PointMark(
+                            x: .value("Time", selectedIOBValue.deliverAt ?? now, unit: .minute),
+                            y: .value("Value", Int(truncating: selectedIOBValue.iob ?? 0))
+                        )
+                        .symbolSize(CGSize(width: 6, height: 6))
+                        .foregroundStyle(Color.primary)
+                    }
+                }
             }
             .frame(minHeight: geo.size.height * 0.12)
             .frame(width: fullWidth(viewWidth: screenSize.width))
             .chartXScale(domain: startMarker ... endMarker)
+            .backport.chartXSelection(value: $selection)
             .chartXAxis { basalChartXAxis }
             .chartYAxis { cobChartYAxis }
             .chartYScale(domain: minValueIobChart ... maxValueIobChart)
@@ -386,10 +509,29 @@ extension MainChartView {
         Chart {
             drawCurrentTimeMarker()
             drawCOB(dummy: false)
+
+            if #available(iOS 17, *) {
+                if let selectedCOBValue {
+                    PointMark(
+                        x: .value("Time", selectedCOBValue.deliverAt ?? now, unit: .minute),
+                        y: .value("Value", selectedCOBValue.cob)
+                    )
+                    .symbolSize(CGSize(width: 15, height: 15))
+                    .foregroundStyle(Color.orange.opacity(0.8))
+
+                    PointMark(
+                        x: .value("Time", selectedCOBValue.deliverAt ?? now, unit: .minute),
+                        y: .value("Value", selectedCOBValue.cob)
+                    )
+                    .symbolSize(CGSize(width: 6, height: 6))
+                    .foregroundStyle(Color.primary)
+                }
+            }
         }
         .frame(minHeight: geo.size.height * 0.12)
         .frame(width: fullWidth(viewWidth: screenSize.width))
         .chartXScale(domain: startMarker ... endMarker)
+        .backport.chartXSelection(value: $selection)
         .chartXAxis { basalChartXAxis }
         .chartYAxis { cobChartYAxis }
         .chartYScale(domain: minValueCobChart ... maxValueCobChart)
@@ -405,7 +547,7 @@ extension MainChartView {
             let bolusDate = insulin.timestamp ?? Date()
 
             if amount != 0, let glucose = timeToNearestGlucose(time: bolusDate.timeIntervalSince1970)?.glucose {
-                let yPosition = (Decimal(glucose) * conversionFactor) + bolusOffset
+                let yPosition = (units == .mgdL ? Decimal(glucose) : Decimal(glucose).asMmolL) + bolusOffset
                 let size = (Config.bolusSize + CGFloat(truncating: amount) * Config.bolusScale) * 1.8
 
                 PointMark(
@@ -418,7 +560,7 @@ extension MainChartView {
                 .annotation(position: .top) {
                     Text(bolusFormatter.string(from: amount) ?? "")
                         .font(.caption2)
-                        .foregroundStyle(Color.insulin)
+                        .foregroundStyle(Color.primary)
                 }
             }
         }
@@ -431,20 +573,21 @@ extension MainChartView {
             let carbDate = carb.date ?? Date()
 
             if let glucose = timeToNearestGlucose(time: carbDate.timeIntervalSince1970)?.glucose {
-                let yPosition = (Decimal(glucose) * conversionFactor) - bolusOffset
+                let yPosition = (units == .mgdL ? Decimal(glucose) : Decimal(glucose).asMmolL) - bolusOffset
                 let size = (Config.carbsSize + CGFloat(carbAmount) * Config.carbsScale)
+                let limitedSize = size > 30 ? 30 : size
 
                 PointMark(
                     x: .value("Time", carbDate, unit: .second),
                     y: .value("Value", yPosition)
                 )
                 .symbol {
-                    Image(systemName: "arrowtriangle.down.fill").font(.system(size: size)).foregroundStyle(Color.orange)
+                    Image(systemName: "arrowtriangle.down.fill").font(.system(size: limitedSize)).foregroundStyle(Color.orange)
                         .rotationEffect(.degrees(180))
                 }
                 .annotation(position: .bottom) {
                     Text(carbsFormatter.string(from: carbAmount as NSNumber)!).font(.caption2)
-                        .foregroundStyle(Color.orange)
+                        .foregroundStyle(Color.primary)
                 }
             }
         }
@@ -465,33 +608,61 @@ extension MainChartView {
             .foregroundStyle(Color.brown)
         }
     }
-
-    // Auggie TODO: use dynamic BG color function
+  
+    private var stops: [Gradient.Stop] {
+        let low = Double(lowGlucose)
+        let high = Double(highGlucose)
+
+        let glucoseValues = state.glucoseFromPersistence
+            .map { units == .mgdL ? Decimal($0.glucose) : Decimal($0.glucose).asMmolL }
+
+        let minimum = glucoseValues.min() ?? 0.0
+        let maximum = glucoseValues.max() ?? 0.0
+
+        // Calculate positions for gradient
+        let lowPosition = (low - Double(truncating: minimum as NSNumber)) /
+            (Double(truncating: maximum as NSNumber) - Double(truncating: minimum as NSNumber))
+        let highPosition = (high - Double(truncating: minimum as NSNumber)) /
+            (Double(truncating: maximum as NSNumber) - Double(truncating: minimum as NSNumber))
+
+        // Ensure positions are in bounds [0, 1]
+        let clampedLowPosition = max(0.0, min(lowPosition, 1.0))
+        let clampedHighPosition = max(0.0, min(highPosition, 1.0))
+
+        // Ensure lowPosition is less than highPosition
+        let sortedPositions = [clampedLowPosition, clampedHighPosition].sorted()
+
+        return [
+            Gradient.Stop(color: .red, location: 0.0),
+            Gradient.Stop(color: .red, location: sortedPositions[0]), // draw red gradient till lowGlucose
+            Gradient.Stop(color: .green, location: sortedPositions[0] + 0.0001), // draw green above lowGlucose till highGlucose
+            Gradient.Stop(color: .green, location: sortedPositions[1]),
+            Gradient.Stop(color: .orange, location: sortedPositions[1] + 0.0001), // draw orange above highGlucose
+            Gradient.Stop(color: .orange, location: 1.0)
+        ]
+    }
+  
     private func drawGlucose(dummy _: Bool) -> some ChartContent {
         ForEach(state.glucoseFromPersistence) { item in
-            let glucoseLevel = Int(item.glucose)
+            let glucoseToDisplay = units == .mgdL ? Decimal(item.glucose) : Decimal(item.glucose).asMmolL
             let color = setBGColor(
                 bgValue: glucoseLevel,
                 highBGColorValue: highGlucose,
                 lowBGColorValue: lowGlucose,
                 dynamicBGColor: dynamicBGColor
             )
-
+                                               
             if smooth {
-                PointMark(
-                    x: .value("Time", item.date ?? Date(), unit: .second),
-                    y: .value("Value", Decimal(glucoseLevel) * conversionFactor)
-                )
-                .foregroundStyle(color)
-                .symbolSize(20)
-                .interpolationMethod(.cardinal)
+                LineMark(x: .value("Time", item.date ?? Date()), y: .value("Value", glucoseToDisplay))
+                    .foregroundStyle(
+                        .linearGradient(stops: stops, startPoint: .bottom, endPoint: .top)
+                    )
+                    .symbol(.circle).symbolSize(34)
             } else {
                 PointMark(
                     x: .value("Time", item.date ?? Date(), unit: .second),
-                    y: .value("Value", Decimal(glucoseLevel) * conversionFactor)
-                )
-                .foregroundStyle(color)
-                .symbolSize(20)
+                    y: .value("Value", glucoseToDisplay)
+                ).foregroundStyle(color).symbolSize(20)
             }
         }
     }
@@ -502,18 +673,66 @@ extension MainChartView {
         return currentTime.addingTimeInterval(timeInterval)
     }
 
-    private func drawForecasts() -> some ChartContent {
+    private func drawForecastsCone() -> some ChartContent {
+        // Draw AreaMark for the forecast bounds
+        ForEach(0 ..< max(state.minForecast.count, state.maxForecast.count), id: \.self) { index in
+            if index < state.minForecast.count, index < state.maxForecast.count {
+                let yMinMaxDelta = Decimal(state.minForecast[index] - state.maxForecast[index])
+                let xValue = timeForIndex(Int32(index))
+
+                // if distance between respective min and max is 0, provide a default range
+                if yMinMaxDelta == 0 {
+                    let yMinValue = units == .mgdL ? Decimal(state.minForecast[index] - 1) :
+                        Decimal(state.minForecast[index] - 1)
+                        .asMmolL
+                    let yMaxValue = units == .mgdL ? Decimal(state.minForecast[index] + 1) :
+                        Decimal(state.minForecast[index] + 1)
+                        .asMmolL
+
+                    if xValue <= Date(timeIntervalSinceNow: TimeInterval(hours: 2.5)) {
+                        AreaMark(
+                            x: .value("Time", xValue),
+                            // maxValue is already parsed to user units, no need to parse
+                            yStart: .value("Min Value", yMinValue <= maxValue ? yMinValue : maxValue),
+                            yEnd: .value("Max Value", yMaxValue <= maxValue ? yMaxValue : maxValue)
+                        )
+                        .foregroundStyle(Color.blue.opacity(0.5))
+                        .interpolationMethod(.catmullRom)
+                    }
+                } else {
+                    let yMinValue = units == .mgdL ? Decimal(state.minForecast[index]) : Decimal(state.minForecast[index]).asMmolL
+                    let yMaxValue = units == .mgdL ? Decimal(state.maxForecast[index]) : Decimal(state.maxForecast[index]).asMmolL
+
+                    if xValue <= Date(timeIntervalSinceNow: TimeInterval(hours: 2.5)) {
+                        AreaMark(
+                            x: .value("Time", xValue),
+                            // maxValue is already parsed to user units, no need to parse
+                            yStart: .value("Min Value", yMinValue <= maxValue ? yMinValue : maxValue),
+                            yEnd: .value("Max Value", yMaxValue <= maxValue ? yMaxValue : maxValue)
+                        )
+                        .foregroundStyle(Color.blue.opacity(0.5))
+                        .interpolationMethod(.catmullRom)
+                    }
+                }
+            }
+        }
+    }
+
+    private func drawForecastsLines() -> some ChartContent {
         ForEach(state.preprocessedData, id: \.id) { tuple in
             let forecastValue = tuple.forecastValue
             let forecast = tuple.forecast
             let valueAsDecimal = Decimal(forecastValue.value)
             let displayValue = units == .mmolL ? valueAsDecimal.asMmolL : valueAsDecimal
+            let xValue = timeForIndex(forecastValue.index)
 
-            LineMark(
-                x: .value("Time", timeForIndex(forecastValue.index)),
-                y: .value("Value", displayValue)
-            )
-            .foregroundStyle(by: .value("Predictions", forecast.type ?? ""))
+            if xValue <= Date(timeIntervalSinceNow: TimeInterval(hours: 2.5)) {
+                LineMark(
+                    x: .value("Time", xValue),
+                    y: .value("Value", displayValue)
+                )
+                .foregroundStyle(by: .value("Predictions", forecast.type ?? ""))
+            }
         }
     }
 
@@ -606,10 +825,10 @@ extension MainChartView {
     private func drawManualGlucose() -> some ChartContent {
         /// manual glucose mark
         ForEach(state.manualGlucoseFromPersistence) { item in
-            let manualGlucose = item.glucose
+            let manualGlucose = units == .mgdL ? Decimal(item.glucose) : Decimal(item.glucose).asMmolL
             PointMark(
                 x: .value("Time", item.date ?? Date(), unit: .second),
-                y: .value("Value", Decimal(manualGlucose) * conversionFactor)
+                y: .value("Value", manualGlucose)
             )
             .symbol {
                 Image(systemName: "drop.fill").font(.system(size: 10)).symbolRenderingMode(.monochrome)
@@ -650,7 +869,8 @@ extension MainChartView {
 
     private func drawIOB() -> some ChartContent {
         ForEach(state.enactedAndNonEnactedDeterminations) { iob in
-            let amount: Double = (iob.iob?.doubleValue ?? 0 / interpolationFactor)
+            let rawAmount = iob.iob?.doubleValue ?? 0
+            let amount: Double = rawAmount > 0 ? rawAmount : rawAmount * 2 // weigh negative iob with factor 2
             let date: Date = iob.deliverAt ?? Date()
 
             LineMark(x: .value("Time", date), y: .value("Amount", amount))
@@ -867,9 +1087,10 @@ extension MainChartView {
             isTempTargetActive = firstNonZeroTarget.createdAt <= now && now <= end
 
             if firstNonZeroTarget.targetTop != nil {
+                let targetTop = firstNonZeroTarget.targetTop ?? 0
                 calculatedTTs
                     .append(ChartTempTarget(
-                        amount: (firstNonZeroTarget.targetTop ?? 0) * conversionFactor,
+                        amount: units == .mgdL ? targetTop : targetTop.asMmolL,
                         start: firstNonZeroTarget.createdAt,
                         end: end
                     ))
@@ -940,7 +1161,18 @@ extension MainChartView {
     /// update start and  end marker to fix scroll update problem with x axis
     private func updateStartEndMarkers() {
         startMarker = Date(timeIntervalSince1970: TimeInterval(NSDate().timeIntervalSince1970 - 86400))
-        endMarker = Date(timeIntervalSince1970: TimeInterval(NSDate().timeIntervalSince1970 + 10800))
+
+        let threeHourSinceNow = Date(timeIntervalSinceNow: TimeInterval(hours: 3))
+
+        // min is 1.5h -> (1.5*1h = 1.5*(5*12*60))
+        let dynamicFutureDateForCone = Date(timeIntervalSinceNow: TimeInterval(
+            Int(1.5) * 5 * state
+                .minCount * 60
+        ))
+
+        endMarker = state
+            .displayForecastsAsLines ? threeHourSinceNow : dynamicFutureDateForCone <= threeHourSinceNow ?
+            dynamicFutureDateForCone.addingTimeInterval(TimeInterval(minutes: 30)) : threeHourSinceNow
     }
 
     private func calculateBasals() {
@@ -982,19 +1214,19 @@ extension MainChartView {
               let minForecast = forecastValues.min(), let maxForecast = forecastValues.max()
         else {
             // default values
-            minValue = 45 * conversionFactor - 20 * conversionFactor
-            maxValue = 270 * conversionFactor + 50 * conversionFactor
+            minValue = 45 - 20
+            maxValue = 270 + 50
             return
         }
 
-        let minOverall = min(minGlucose, minForecast)
-        let maxOverall = max(maxGlucose, maxForecast)
+        // Ensure maxForecast is not more than 100 over maxGlucose
+        let adjustedMaxForecast = min(maxForecast, maxGlucose + 100)
 
-        minValue = minOverall * conversionFactor - 50 * conversionFactor
-        maxValue = maxOverall * conversionFactor + 80 * conversionFactor
+        var minOverall = min(minGlucose, minForecast)
+        var maxOverall = max(maxGlucose, adjustedMaxForecast)
 
-        debug(.default, "min \(minValue)")
-        debug(.default, "max \(maxValue)")
+        minValue = minOverall - 50
+        maxValue = maxOverall + 80
     }
 
     private func yAxisChartDataCobChart() {
@@ -1027,7 +1259,6 @@ extension MainChartView {
         plotContent
             .rotationEffect(.degrees(180))
             .scaleEffect(x: -1, y: 1)
-            .chartXAxis(.hidden)
     }
 
     private var mainChartXAxis: some AxisContent {
@@ -1055,7 +1286,7 @@ extension MainChartView {
     private var mainChartYAxis: some AxisContent {
         AxisMarks(position: .trailing) { value in
 
-            if displayXgridLines {
+            if displayYgridLines {
                 AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
             } else {
                 AxisGridLine(stroke: .init(lineWidth: 0, dash: [2, 3]))
@@ -1073,7 +1304,7 @@ extension MainChartView {
 
     private var cobChartYAxis: some AxisContent {
         AxisMarks(position: .trailing) { _ in
-            if displayXgridLines {
+            if displayYgridLines {
                 AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
             } else {
                 AxisGridLine(stroke: .init(lineWidth: 0, dash: [2, 3]))

+ 9 - 14
FreeAPS/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift

@@ -84,13 +84,13 @@ struct CurrentGlucoseView: View {
                 VStack(alignment: .center) {
                     HStack {
                         if let glucoseValue = combinedGlucoseValues.first?.glucose {
-                            let displayGlucose = convertGlucose(glucoseValue, to: units)
+                            let displayGlucose = units == .mgdL ? Decimal(glucoseValue).description : Decimal(glucoseValue)
+                                .formattedAsMmolL
                             Text(
-                                glucoseValue == 400 ? "HIGH" :
-                                    glucoseFormatter.string(from: NSNumber(value: displayGlucose)) ?? "--"
+                                glucoseValue == 400 ? "HIGH" : displayGlucose
                             )
                             .font(.system(size: 40, weight: .bold, design: .rounded))
-                            .foregroundColor(alarm == nil ? colourGlucoseText : .loopRed)
+                            .foregroundColor(alarm == nil ? glucoseDisplayColor : .loopRed)
                         } else {
                             Text("--")
                                 .font(.system(size: 40, weight: .bold, design: .rounded))
@@ -155,15 +155,6 @@ struct CurrentGlucoseView: View {
         }
     }
 
-    private func convertGlucose(_ value: Int16, to units: GlucoseUnits) -> Double {
-        switch units {
-        case .mmolL:
-            return Double(value) / 18.0
-        case .mgdL:
-            return Double(value)
-        }
-    }
-
     private var delta: String {
         guard combinedGlucoseValues.count >= 2 else {
             return "--"
@@ -176,13 +167,17 @@ struct CurrentGlucoseView: View {
         return deltaFormatter.string(from: deltaAsDecimal as NSNumber) ?? "--"
     }
 
-    var colourGlucoseText: Color {
+    var glucoseDisplayColor: Color {
         // Fetch the first glucose reading and convert it to Int for comparison
         let whichGlucose = Int(combinedGlucoseValues.first?.glucose ?? 0)
 
         // Define default color based on the color scheme
         let defaultColor: Color = colorScheme == .dark ? .white : .black
 
+        // low and high glucose is parsed in state to mmol/L; parse it back to mg/dl here for comparison
+        let lowGlucose = units == .mgdL ? lowGlucose : lowGlucose.asMgdL
+        let highGlucose = units == .mgdL ? highGlucose : highGlucose.asMgdL
+
         // Ensure the thresholds are logical
         guard lowGlucose < highGlucose else { return .primary }
 

+ 44 - 32
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -410,7 +410,7 @@ extension Home {
                 }
                 /// eventualBG string at bottomTrailing
 
-                if let eventualBG = state.determinationsFromPersistence.first?.eventualBG {
+                if let eventualBG = state.enactedAndNonEnactedDeterminations.first?.eventualBG {
                     let bg = eventualBG as Decimal
                     HStack {
                         Image(systemName: "arrow.right.circle")
@@ -698,7 +698,7 @@ extension Home {
                         Spacer()
 
                         Button {
-                            state.waitForSuggestion = true
+                            state.showProgressView()
                             state.cancelBolus()
                         } label: {
                             Image(systemName: "xmark.app")
@@ -788,30 +788,42 @@ extension Home {
                     .font(.subheadline)
                     .foregroundColor(.secondary)
 
-                    List {
-                        DefinitionRow(
-                            term: "IOB (Insulin on Board)",
-                            definition: "Forecasts BG based on the amount of insulin still active in the body.",
-                            color: .insulin
-                        )
-                        DefinitionRow(
-                            term: "ZT (Zero-Temp)",
-                            definition: "Forecasts the worst-case blood glucose (BG) scenario if no carbs are absorbed and insulin delivery is stopped until BG starts rising.",
-                            color: .zt
-                        )
-                        DefinitionRow(
-                            term: "COB (Carbs on Board)",
-                            definition: "Forecasts BG changes by considering the amount of carbohydrates still being absorbed in the body.",
-                            color: .loopYellow
-                        )
-                        DefinitionRow(
-                            term: "UAM (Unannounced Meal)",
-                            definition: "Forecasts BG levels and insulin dosing needs for unexpected meals or other causes of BG rises without prior notice.",
-                            color: .uam
-                        )
+                    if state.settingsManager.settings.displayForecastsAsLines {
+                        List {
+                            DefinitionRow(
+                                term: "IOB (Insulin on Board)",
+                                definition: "Forecasts BG based on the amount of insulin still active in the body.",
+                                color: .insulin
+                            )
+                            DefinitionRow(
+                                term: "ZT (Zero-Temp)",
+                                definition: "Forecasts the worst-case blood glucose (BG) scenario if no carbs are absorbed and insulin delivery is stopped until BG starts rising.",
+                                color: .zt
+                            )
+                            DefinitionRow(
+                                term: "COB (Carbs on Board)",
+                                definition: "Forecasts BG changes by considering the amount of carbohydrates still being absorbed in the body.",
+                                color: .loopYellow
+                            )
+                            DefinitionRow(
+                                term: "UAM (Unannounced Meal)",
+                                definition: "Forecasts BG levels and insulin dosing needs for unexpected meals or other causes of BG rises without prior notice.",
+                                color: .uam
+                            )
+                        }
+                        .padding(.trailing, 10)
+                        .navigationBarTitle("Legend", displayMode: .inline)
+                    } else {
+                        List {
+                            DefinitionRow(
+                                term: "Cone of Uncertainty",
+                                definition: "For simplicity reasons, oref's various forecast curves are displayed as a \"Cone of Uncertainty\" that depicts a possible, forecasted range of future glucose fluctuation based on the current data and the algothim's result.",
+                                color: Color.blue.opacity(0.5)
+                            )
+                        }
+                        .padding(.trailing, 10)
+                        .navigationBarTitle("Legend", displayMode: .inline)
                     }
-                    .padding(.trailing, 10)
-                    .navigationBarTitle("Legend", displayMode: .inline)
 
                     Button { state.isLegendPresented.toggle() }
                     label: { Text("Got it!").frame(maxWidth: .infinity, alignment: .center) }
@@ -832,15 +844,15 @@ extension Home {
             ZStack(alignment: .bottom) {
                 TabView(selection: $selectedTab) {
                     let carbsRequiredBadge: String? = {
-                        guard let carbsRequired = state.determinationsFromPersistence.first?.carbsRequired as? Decimal
-                        else { return nil }
-                        if carbsRequired > state.settingsManager.settings.carbsRequiredThreshold {
-                            let numberAsNSNumber = NSDecimalNumber(decimal: carbsRequired)
-                            let formattedNumber = numberFormatter.string(from: numberAsNSNumber) ?? ""
-                            return formattedNumber + " g"
-                        } else {
+                        guard let carbsRequired = state.enactedAndNonEnactedDeterminations.first?.carbsRequired else {
                             return nil
                         }
+                        let carbsRequiredDecimal = Decimal(carbsRequired)
+                        if carbsRequiredDecimal > state.settingsManager.settings.carbsRequiredThreshold {
+                            let numberAsNSNumber = NSDecimalNumber(decimal: carbsRequiredDecimal)
+                            return (numberFormatter.string(from: numberAsNSNumber) ?? "") + " g"
+                        }
+                        return nil
                     }()
 
                     NavigationStack { mainView() }

+ 3 - 14
FreeAPS/Sources/Modules/OverrideConfig/View/OverrideRootView.swift

@@ -51,17 +51,6 @@ extension OverrideConfig {
             return formatter
         }
 
-        private var glucoseFormatter: NumberFormatter {
-            let formatter = NumberFormatter()
-            formatter.numberStyle = .decimal
-            formatter.maximumFractionDigits = 0
-            if state.units == .mmolL {
-                formatter.maximumFractionDigits = 1
-            }
-            formatter.roundingMode = .halfUp
-            return formatter
-        }
-
         var body: some View {
             VStack {
                 Picker("Tab", selection: $state.selectedTab) {
@@ -449,8 +438,8 @@ extension OverrideConfig {
         }
 
         @ViewBuilder private func overridesView(for preset: OverrideStored) -> some View {
-            let target = state.units == .mmolL ? (((preset.target ?? 0) as NSDecimalNumber) as Decimal)
-                .asMmolL : (preset.target ?? 0) as Decimal
+            let target = (state.units == .mgdL ? preset.target : preset.target?.decimalValue.asMmolL as NSDecimalNumber?) ?? 0
+
             let duration = (preset.duration ?? 0) as Decimal
             let name = ((preset.name ?? "") == "") || (preset.name?.isEmpty ?? true) ? "" : preset.name!
             let percent = preset.percentage / 100
@@ -458,7 +447,7 @@ extension OverrideConfig {
             let durationString = perpetual ? "" : "\(formatter.string(from: duration as NSNumber)!)"
             let scheduledSMBstring = (preset.smbIsOff && preset.smbIsAlwaysOff) ? "Scheduled SMBs" : ""
             let smbString = (preset.smbIsOff && scheduledSMBstring == "") ? "SMBs are off" : ""
-            let targetString = target != 0 ? "\(glucoseFormatter.string(from: target as NSNumber)!)" : ""
+            let targetString = target != 0 ? target.description : ""
             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" : ""

+ 27 - 23
FreeAPS/Sources/Modules/Stat/StatStateModel.swift

@@ -45,31 +45,35 @@ extension Stat {
         }
 
         private func fetchGlucose(for duration: Duration) async -> [NSManagedObjectID] {
-            await context.perform {
-                let predicate: NSPredicate
+            let predicate: NSPredicate
 
-                switch duration {
-                case .Day:
-                    predicate = NSPredicate.glucoseForStatsDay
-                case .Week:
-                    predicate = NSPredicate.glucoseForStatsWeek
-                case .Today:
-                    predicate = NSPredicate.glucoseForStatsToday
-                case .Month:
-                    predicate = NSPredicate.glucoseForStatsMonth
-                case .Total:
-                    predicate = NSPredicate.glucoseForStatsTotal
-                }
+            switch duration {
+            case .Day:
+                predicate = NSPredicate.glucoseForStatsDay
+            case .Week:
+                predicate = NSPredicate.glucoseForStatsWeek
+            case .Today:
+                predicate = NSPredicate.glucoseForStatsToday
+            case .Month:
+                predicate = NSPredicate.glucoseForStatsMonth
+            case .Total:
+                predicate = NSPredicate.glucoseForStatsTotal
+            }
+
+            let results = await CoreDataStack.shared.fetchEntitiesAsync(
+                ofType: GlucoseStored.self,
+                onContext: context,
+                predicate: predicate,
+                key: "date",
+                ascending: false,
+                batchSize: 100,
+                propertiesToFetch: ["glucose", "objectID"]
+            )
+
+            guard let fetchedResults = results as? [[String: Any]] else { return [] }
 
-                return CoreDataStack.shared.fetchEntities(
-                    ofType: GlucoseStored.self,
-                    onContext: self.context,
-                    predicate: predicate,
-                    key: "date",
-                    ascending: false,
-                    batchSize: 100,
-                    propertiesToFetch: ["glucose", "date"]
-                ).map(\.objectID)
+            return await context.perform {
+                return fetchedResults.compactMap { $0["objectID"] as? NSManagedObjectID }
             }
         }
 

+ 2 - 0
FreeAPS/Sources/Modules/StatConfig/StatConfigStateModel.swift

@@ -17,6 +17,7 @@ extension StatConfig {
         @Published var yGridLines: Bool = false
         @Published var oneDimensionalGraph = false
         @Published var rulerMarks: Bool = true
+        @Published var displayForecastsAsLines: Bool = false
 
         var units: GlucoseUnits = .mgdL
 
@@ -29,6 +30,7 @@ extension StatConfig {
             subscribeSetting(\.xGridLines, on: $xGridLines) { xGridLines = $0 }
             subscribeSetting(\.yGridLines, on: $yGridLines) { yGridLines = $0 }
             subscribeSetting(\.rulerMarks, on: $rulerMarks) { rulerMarks = $0 }
+            subscribeSetting(\.displayForecastsAsLines, on: $displayForecastsAsLines) { displayForecastsAsLines = $0 }
             subscribeSetting(\.useFPUconversion, on: $useFPUconversion) { useFPUconversion = $0 }
             subscribeSetting(\.tins, on: $tins) { tins = $0 }
             subscribeSetting(\.skipBolusScreenAfterCarbs, on: $skipBolusScreenAfterCarbs) { skipBolusScreenAfterCarbs = $0 }

+ 1 - 0
FreeAPS/Sources/Modules/StatConfig/View/StatConfigRootView.swift

@@ -57,6 +57,7 @@ extension StatConfig {
                         TextFieldWithToolBar(text: $state.hours, placeholder: "6", numberFormatter: carbsFormatter)
                         Text("hours").foregroundColor(.secondary)
                     }
+                    Toggle("Show Forecasts as Lines", isOn: $state.displayForecastsAsLines)
                 } header: { Text("Home Chart settings ") }
 
                 Section {

+ 9 - 3
FreeAPS/Sources/Services/Calendar/CalendarManager.swift

@@ -172,10 +172,13 @@ final class BaseCalendarManager: CalendarManager, Injectable {
             key: "timestamp",
             ascending: false,
             fetchLimit: 1,
-            propertiesToFetch: ["timestamp", "cob", "iob"]
+            propertiesToFetch: ["timestamp", "cob", "iob", "objectID"]
         )
+
+        guard let fetchedResults = results as? [[String: Any]], !fetchedResults.isEmpty else { return nil }
+
         return await backgroundContext.perform {
-            results.first.map(\.objectID)
+            return fetchedResults.first?["objectID"] as? NSManagedObjectID
         }
     }
 
@@ -187,8 +190,11 @@ final class BaseCalendarManager: CalendarManager, Injectable {
             key: "date",
             ascending: false
         )
+
+        guard let fetchedResults = results as? [[String: Any]] else { return [] }
+
         return await backgroundContext.perform {
-            return results.map(\.objectID)
+            return fetchedResults.compactMap { $0["objectID"] as? NSManagedObjectID }
         }
     }
 

+ 3 - 32
FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift

@@ -168,14 +168,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsObserver, P
                 }
 
             healthKitStore.save(samplesToSave) { (success: Bool, error: Error?) -> Void in
-                if success {
-                    for sample in samplesToSave {
-                        debug(
-                            .service,
-                            "Stored blood glucose \(sample.quantity) in HealthKit Store! Metadata: \(String(describing: sample.metadata?.values))"
-                        )
-                    }
-                } else {
+                if !success {
                     debug(.service, "Failed to store blood glucose in HealthKit Store!")
                     debug(.service, error?.localizedDescription ?? "Unknown error")
                 }
@@ -222,14 +215,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsObserver, P
                 }
 
             healthKitStore.save(samplesToSave) { (success: Bool, error: Error?) -> Void in
-                if success {
-                    for sample in samplesToSave {
-                        debug(
-                            .service,
-                            "Stored carb entry \(sample.quantity) in HealthKit Store! Metadata: \(String(describing: sample.metadata?.values))"
-                        )
-                    }
-                } else {
+                if !success {
                     debug(.service, "Failed to store carb entry in HealthKit Store!")
                     debug(.service, error?.localizedDescription ?? "Unknown error")
                 }
@@ -298,14 +284,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsObserver, P
                 }
 
             healthKitStore.save(bolusSamples + basalSamples) { (success: Bool, error: Error?) -> Void in
-                if success {
-                    for sample in bolusSamples + basalSamples {
-                        debug(
-                            .service,
-                            "Stored insulin entry in HealthKit Store! Metadata: \(String(describing: sample.metadata?.values))"
-                        )
-                    }
-                } else {
+                if !success {
                     debug(.service, "Failed to store insulin entry in HealthKit Store!")
                     debug(.service, error?.localizedDescription ?? "Unknown error")
                 }
@@ -493,7 +472,6 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsObserver, P
 
     private func prepareBGSamplesToPublisherFetch(_ samples: [HKQuantitySample]) {
         dispatchPrecondition(condition: .onQueue(processQueue))
-        debug(.service, "Start preparing samples: \(String(describing: samples))")
 
         newGlucose += samples
             .compactMap { sample -> HealthKitSample? in
@@ -522,11 +500,6 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsObserver, P
             .filter { $0.dateString >= Date().addingTimeInterval(-1.days.timeInterval) }
 
         newGlucose = newGlucose.removeDublicates()
-
-        debug(
-            .service,
-            "Current BloodGlucose.Type objects will be send from Publisher during fetch: \(String(describing: newGlucose))"
-        )
     }
 
     // MARK: - GlucoseSource
@@ -542,9 +515,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsObserver, P
             }
 
             self.processQueue.async {
-                //   debug(.service, "Start fetching HealthKitManager")
                 guard self.settingsManager.settings.useAppleHealth else {
-                    debug(.service, "HealthKitManager cant return any data, because useAppleHealth option is disable")
                     promise(.success([]))
                     return
                 }

+ 33 - 9
FreeAPS/Sources/Services/LiveActivity/Data/DataManager.swift

@@ -5,7 +5,7 @@ import Foundation
 @available(iOS 16.2, *)
 extension LiveActivityBridge {
     func fetchAndMapGlucose() async {
-        let result = await CoreDataStack.shared.fetchEntitiesAsync(
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             onContext: context,
             predicate: NSPredicate.predicateForSixHoursAgo,
@@ -13,38 +13,62 @@ extension LiveActivityBridge {
             ascending: false,
             fetchLimit: 72
         )
+
+        guard let glucoseResults = results as? [GlucoseStored] else {
+            return
+        }
+
         await context.perform {
-            self.glucoseFromPersistence = result
-                .map { GlucoseData(glucose: Int($0.glucose), date: $0.date ?? Date(), direction: $0.directionEnum) }
+            self.glucoseFromPersistence = glucoseResults.map {
+                GlucoseData(glucose: Int($0.glucose), date: $0.date ?? Date(), direction: $0.directionEnum)
+            }
         }
     }
 
     func fetchAndMapDetermination() async {
-        let result = await CoreDataStack.shared.fetchEntitiesAsync(
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OrefDetermination.self,
             onContext: context,
             predicate: NSPredicate.predicateFor30MinAgoForDetermination,
             key: "deliverAt",
             ascending: false,
             fetchLimit: 1,
-            propertiesToFetch: ["iob", "cob", "deliverAt"]
+            propertiesToFetch: ["iob", "cob"]
         )
+
+        guard let determinationResults = results as? [[String: Any]] else {
+            return
+        }
+
         await context.perform {
-            self.determination = result.first.map { DeterminationData(cob: Int($0.cob), iob: $0.iob?.decimalValue ?? 0) }
+            self.determination = determinationResults.first.map {
+                DeterminationData(
+                    cob: ($0["cob"] as? Int) ?? 0,
+                    iob: ($0["iob"] as? NSDecimalNumber)?.decimalValue ?? 0
+                )
+            }
         }
     }
 
     func fetchAndMapOverride() async {
-        let result = await CoreDataStack.shared.fetchEntitiesAsync(
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,
             onContext: context,
             predicate: NSPredicate.predicateForOneDayAgo,
             key: "date",
             ascending: false,
-            fetchLimit: 1
+            fetchLimit: 1,
+            propertiesToFetch: ["enabled"]
         )
+
+        guard let overrideResults = results as? [[String: Any]] else {
+            return
+        }
+
         await context.perform {
-            self.isOverridesActive = result.first.map { OverrideData(isActive: $0.enabled) }
+            self.isOverridesActive = overrideResults.first.map {
+                OverrideData(isActive: $0["enabled"] as? Bool ?? false)
+            }
         }
     }
 }

+ 3 - 3
FreeAPS/Sources/Services/LiveActivity/LiveActitiyAttributes.swift

@@ -15,11 +15,11 @@ struct LiveActivityAttributes: ActivityAttributes {
     }
 
     public struct ContentAdditionalState: Codable, Hashable {
-        let chart: [Double]
+        let chart: [Decimal]
         let chartDate: [Date?]
         let rotationDegrees: Double
-        let highGlucose: Double
-        let lowGlucose: Double
+        let highGlucose: Decimal
+        let lowGlucose: Decimal
         let dynamicBGColor: Bool
         let cob: Decimal
         let iob: Decimal

+ 8 - 20
FreeAPS/Sources/Services/LiveActivity/LiveActivityAttributes+Helper.swift

@@ -79,33 +79,21 @@ extension LiveActivityAttributes.ContentState {
 
         switch settings.lockScreenView {
         case .detailed:
-            let chartBG = chart.map(\.glucose)
-
-            let conversionFactor: Double = settings.units == .mmolL ? 18.0 : 1.0
-            let convertedChartBG = chartBG.map { Double($0) / conversionFactor }
-
+            let chartBG = chart.map { Decimal($0.glucose) }
             let chartDate = chart.map(\.date)
 
             /// glucose limits from UI settings, not from notifications settings
-            let highGlucose = settings.high / Decimal(conversionFactor)
-            let lowGlucose = settings.low / Decimal(conversionFactor)
-
-            let cob = determination?.cob ?? 0
-            let iob = determination?.iob ?? 0
-            let unit = settings.units == .mmolL ? " mmol/L" : " mg/dL"
-            let isOverrideActive = override?.isActive ?? false
-
             detailedState = LiveActivityAttributes.ContentAdditionalState(
-                chart: convertedChartBG,
+                chart: chartBG,
                 chartDate: chartDate,
                 rotationDegrees: rotationDegrees,
-                highGlucose: Double(highGlucose),
-                lowGlucose: Double(lowGlucose),
+                highGlucose: settings.high,
+                lowGlucose: settings.low,
                 dynamicBGColor: settings.dynamicBGColor,
-                cob: Decimal(cob),
-                iob: iob as Decimal,
-                unit: unit,
-                isOverrideActive: isOverrideActive
+                cob: Decimal(determination?.cob ?? 0),
+                iob: determination?.iob ?? 0 as Decimal,
+                unit: settings.units.rawValue,
+                isOverrideActive: override?.isActive ?? false
             )
 
         case .simple:

+ 11 - 11
FreeAPS/Sources/Services/Network/NightscoutAPI.swift

@@ -344,8 +344,8 @@ extension NightscoutAPI {
         do {
             let encodedBody = try JSONCoding.encoder.encode(treatments)
             request.httpBody = encodedBody
-            debugPrint("Payload treatments size: \(encodedBody.count) bytes")
-            debugPrint(String(data: encodedBody, encoding: .utf8) ?? "Invalid payload")
+//            debugPrint("Payload treatments size: \(encodedBody.count) bytes")
+//            debugPrint(String(data: encodedBody, encoding: .utf8) ?? "Invalid payload")
         } catch {
             debugPrint("Error encoding payload: \(error.localizedDescription)")
             throw error
@@ -359,7 +359,7 @@ extension NightscoutAPI {
             throw URLError(.badServerResponse)
         }
 
-        debugPrint("Upload successful, response data: \(String(data: data, encoding: .utf8) ?? "No data")")
+//        debugPrint("Upload successful, response data: \(String(data: data, encoding: .utf8) ?? "No data")")
     }
 
     func uploadGlucose(_ glucose: [BloodGlucose]) async throws {
@@ -380,8 +380,8 @@ extension NightscoutAPI {
         do {
             let encodedBody = try JSONCoding.encoder.encode(glucose)
             request.httpBody = encodedBody
-            debugPrint("Payload glucose size: \(encodedBody.count) bytes")
-            debugPrint(String(data: encodedBody, encoding: .utf8) ?? "Invalid payload")
+//            debugPrint("Payload glucose size: \(encodedBody.count) bytes")
+//            debugPrint(String(data: encodedBody, encoding: .utf8) ?? "Invalid payload")
         } catch {
             debugPrint("Error encoding payload: \(error.localizedDescription)")
             throw error
@@ -395,7 +395,7 @@ extension NightscoutAPI {
             throw URLError(.badServerResponse)
         }
 
-        debugPrint("Upload successful, response data: \(String(data: data, encoding: .utf8) ?? "No data")")
+//        debugPrint("Upload successful, response data: \(String(data: data, encoding: .utf8) ?? "No data")")
     }
 
     func uploadStats(_ stats: NightscoutStatistics) async throws {
@@ -442,8 +442,8 @@ extension NightscoutAPI {
         do {
             let encodedBody = try JSONCoding.encoder.encode(status)
             request.httpBody = encodedBody
-            debugPrint("Payload glucose size: \(encodedBody.count) bytes")
-            debugPrint(String(data: encodedBody, encoding: .utf8) ?? "Invalid payload")
+//            debugPrint("Payload glucose size: \(encodedBody.count) bytes")
+//            debugPrint(String(data: encodedBody, encoding: .utf8) ?? "Invalid payload")
         } catch {
             debugPrint("Error encoding payload: \(error.localizedDescription)")
             throw error
@@ -572,8 +572,8 @@ extension NightscoutAPI {
         do {
             let encodedBody = try JSONCoding.encoder.encode(overrides)
             request.httpBody = encodedBody
-            debugPrint("Payload glucose size: \(encodedBody.count) bytes")
-            debugPrint(String(data: encodedBody, encoding: .utf8) ?? "Invalid payload")
+//            debugPrint("Payload glucose size: \(encodedBody.count) bytes")
+//            debugPrint(String(data: encodedBody, encoding: .utf8) ?? "Invalid payload")
         } catch {
             debugPrint("Error encoding payload: \(error.localizedDescription)")
             throw error
@@ -587,7 +587,7 @@ extension NightscoutAPI {
             throw URLError(.badServerResponse)
         }
 
-        debugPrint("Upload successful, response data: \(String(data: data, encoding: .utf8) ?? "No data")")
+//        debugPrint("Upload successful, response data: \(String(data: data, encoding: .utf8) ?? "No data")")
     }
 }
 

+ 0 - 14
FreeAPS/Sources/Services/Network/NightscoutManager.swift

@@ -482,13 +482,11 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     private func updateOrefDeterminationAsUploaded(_ determination: [Determination]) async {
         await backgroundContext.perform {
             let ids = determination.map(\.id) as NSArray
-            print("\(DebuggingIdentifiers.inProgress) ids: \(ids)")
             let fetchRequest: NSFetchRequest<OrefDetermination> = OrefDetermination.fetchRequest()
             fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
 
             do {
                 let results = try self.backgroundContext.fetch(fetchRequest)
-                print("\(DebuggingIdentifiers.inProgress) results: \(results)")
                 for result in results {
                     result.isUploadedToNS = true
                 }
@@ -737,13 +735,11 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     private func updateGlucoseAsUploaded(_ glucose: [BloodGlucose]) async {
         await backgroundContext.perform {
             let ids = glucose.map(\.id) as NSArray
-            print("\(DebuggingIdentifiers.inProgress) ids: \(ids)")
             let fetchRequest: NSFetchRequest<GlucoseStored> = GlucoseStored.fetchRequest()
             fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
 
             do {
                 let results = try self.backgroundContext.fetch(fetchRequest)
-                print("\(DebuggingIdentifiers.inProgress) results: \(results)")
                 for result in results {
                     result.isUploadedToNS = true
                 }
@@ -780,13 +776,11 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     private func updateTreatmentsAsUploaded(_ treatments: [NightscoutTreatment]) async {
         await backgroundContext.perform {
             let ids = treatments.map(\.id) as NSArray
-            print("\(DebuggingIdentifiers.inProgress) ids: \(ids)")
             let fetchRequest: NSFetchRequest<PumpEventStored> = PumpEventStored.fetchRequest()
             fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
 
             do {
                 let results = try self.backgroundContext.fetch(fetchRequest)
-                print("\(DebuggingIdentifiers.inProgress) results: \(results)")
                 for result in results {
                     result.isUploadedToNS = true
                 }
@@ -823,13 +817,11 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     private func updateManualGlucoseAsUploaded(_ treatments: [NightscoutTreatment]) async {
         await backgroundContext.perform {
             let ids = treatments.map(\.id) as NSArray
-            print("\(DebuggingIdentifiers.inProgress) ids: \(ids)")
             let fetchRequest: NSFetchRequest<GlucoseStored> = GlucoseStored.fetchRequest()
             fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
 
             do {
                 let results = try self.backgroundContext.fetch(fetchRequest)
-                print("\(DebuggingIdentifiers.inProgress) results: \(results)")
                 for result in results {
                     result.isUploadedToNS = true
                 }
@@ -866,13 +858,11 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     private func updateCarbsAsUploaded(_ treatments: [NightscoutTreatment]) async {
         await backgroundContext.perform {
             let ids = treatments.map(\.id) as NSArray
-            print("\(DebuggingIdentifiers.inProgress) ids: \(ids)")
             let fetchRequest: NSFetchRequest<CarbEntryStored> = CarbEntryStored.fetchRequest()
             fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
 
             do {
                 let results = try self.backgroundContext.fetch(fetchRequest)
-                print("\(DebuggingIdentifiers.inProgress) results: \(results)")
                 for result in results {
                     result.isUploadedToNS = true
                 }
@@ -909,13 +899,11 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     private func updateOverridesAsUploaded(_ overrides: [NightscoutExercise]) async {
         await backgroundContext.perform {
             let ids = overrides.map(\.id) as NSArray
-            print("\(DebuggingIdentifiers.inProgress) ids: \(ids)")
             let fetchRequest: NSFetchRequest<OverrideStored> = OverrideStored.fetchRequest()
             fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
 
             do {
                 let results = try self.backgroundContext.fetch(fetchRequest)
-                print("\(DebuggingIdentifiers.inProgress) results: \(results)")
                 for result in results {
                     result.isUploadedToNS = true
                 }
@@ -952,13 +940,11 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     private func updateOverrideRunsAsUploaded(_ overrideRuns: [NightscoutExercise]) async {
         await backgroundContext.perform {
             let ids = overrideRuns.map(\.id) as NSArray
-            print("\(DebuggingIdentifiers.inProgress) ids: \(ids)")
             let fetchRequest: NSFetchRequest<OverrideRunStored> = OverrideRunStored.fetchRequest()
             fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
 
             do {
                 let results = try self.backgroundContext.fetch(fetchRequest)
-                print("\(DebuggingIdentifiers.inProgress) results: \(results)")
                 for result in results {
                     result.isUploadedToNS = true
                 }

+ 5 - 2
FreeAPS/Sources/Services/UserNotifications/UserNotificationsManager.swift

@@ -225,8 +225,11 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
             ascending: false,
             fetchLimit: 3
         )
+
+        guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+
         return await backgroundContext.perform {
-            return results.map(\.objectID)
+            return fetchedResults.map(\.objectID)
         }
     }
 
@@ -240,7 +243,7 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
 
             guard let lastReading = glucoseObjects.first?.glucose,
                   let secondLastReading = glucoseObjects.dropFirst().first?.glucose,
-                  let lastDirection = glucoseObjects.first?.direction else { return }
+                  let lastDirection = glucoseObjects.first?.directionEnum?.symbol else { return }
 
             addAppBadge(glucose: (glucoseObjects.first?.glucose).map { Int($0) })
 

+ 117 - 101
FreeAPS/Sources/Services/WatchManager/WatchManager.swift

@@ -67,6 +67,7 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
         setupNotification()
         coreDataObserver = CoreDataObserver()
         registerHandlers()
+
         Task {
             await configureState()
         }
@@ -91,10 +92,6 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
             }
             return data
         }
-
-        Task {
-            await configureState()
-        }
     }
 
     func setupNotification() {
@@ -142,12 +139,13 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
             predicate: NSPredicate.enactedDetermination,
             key: "timestamp",
             ascending: false,
-            fetchLimit: 1,
-            propertiesToFetch: ["timestamp"]
+            fetchLimit: 1
         )
 
+        guard let fetchedResults = results as? [OrefDetermination] else { return [] }
+
         return await context.perform {
-            results.map(\.objectID)
+            fetchedResults.map(\.objectID)
         }
     }
 
@@ -158,11 +156,14 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
             predicate: NSPredicate.predicateForOneDayAgo,
             key: "date",
             ascending: false,
-            fetchLimit: 1
+            fetchLimit: 1,
+            propertiesToFetch: ["enabled", "percentage", "objectID"]
         )
 
+        guard let fetchedResults = results as? [[String: Any]] else { return nil }
+
         return await context.perform {
-            results.map(\.objectID).first
+            fetchedResults.compactMap { $0["objectID"] as? NSManagedObjectID }.first
         }
     }
 
@@ -177,116 +178,131 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
             batchSize: 12
         )
 
+        guard let glucoseResults = results as? [GlucoseStored] else {
+            return []
+        }
+
         return await context.perform {
-            results.map(\.objectID)
+            glucoseResults.map(\.objectID)
         }
     }
 
-    @MainActor private func configureState() async {
+    private func configureState() async {
         let glucoseValuesIDs = await fetchGlucose()
-        guard let lastDeterminationID = await fetchlastDetermination().first,
-              let latestOverrideID = await fetchLatestOverride() else { return }
+        async let lastDeterminationIDs = fetchlastDetermination()
+        async let latestOverrideID = fetchLatestOverride()
+
+        guard let lastDeterminationID = await lastDeterminationIDs.first,
+              let latestOverrideID = await latestOverrideID
+        else {
+            debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to get last Determination/ last Override")
+            return
+        }
 
         do {
-            let glucoseValues = try glucoseValuesIDs.compactMap { id in
-                try viewContext.existingObject(with: id) as? GlucoseStored
-            }
+            let glucoseValues: [GlucoseStored] = await CoreDataStack.shared
+                .getNSManagedObject(with: glucoseValuesIDs, context: viewContext)
 
             let lastDetermination = try viewContext.existingObject(with: lastDeterminationID) as? OrefDetermination
             let latestOverride = try viewContext.existingObject(with: latestOverrideID) as? OverrideStored
 
-            if let firstGlucoseValue = glucoseValues.first {
-                let value = settingsManager.settings
-                    .units == .mgdL ? Decimal(firstGlucoseValue.glucose) : Decimal(firstGlucoseValue.glucose).asMmolL
-                state.glucose = glucoseFormatter.string(from: value as NSNumber)
-                state.trend = firstGlucoseValue.direction
-                let delta = glucoseValues
-                    .count >= 2 ? Decimal(firstGlucoseValue.glucose) - Decimal(glucoseValues.dropFirst().first?.glucose ?? 0) : 0
-                let deltaConverted = settingsManager.settings.units == .mgdL ? delta : delta.asMmolL
-                state.delta = deltaFormatter.string(from: deltaConverted as NSNumber)
-                state.trendRaw = firstGlucoseValue.direction
-                state.glucoseDate = firstGlucoseValue.date
-            }
-
-            state.lastLoopDate = lastDetermination?.timestamp
-            state.lastLoopDateInterval = state.lastLoopDate.map {
-                guard $0.timeIntervalSince1970 > 0 else { return 0 }
-                return UInt64($0.timeIntervalSince1970)
-            }
-            state.bolusIncrement = settingsManager.preferences.bolusIncrement
-            state.maxCOB = settingsManager.preferences.maxCOB
-            state.maxBolus = settingsManager.pumpSettings.maxBolus
-            state.carbsRequired = lastDetermination?.carbsRequired as? Decimal
-
-            var insulinRequired = lastDetermination?.insulinReq as? Decimal ?? 0
-
-            var double: Decimal = 2
-            if lastDetermination?.manualBolusErrorString == 0 {
-                insulinRequired = lastDetermination?.insulinForManualBolus as? Decimal ?? 0
-                double = 1
-            }
-
-            state.useNewCalc = settingsManager.settings.useCalc
+            let recommendedInsulin = await newBolusCalc(
+                ids: glucoseValuesIDs,
+                determination: lastDetermination
+            )
+
+            await MainActor.run { [weak self] in
+                guard let self = self else { return }
+
+                if let firstGlucoseValue = glucoseValues.first {
+                    let value = self.settingsManager.settings.units == .mgdL
+                        ? Decimal(firstGlucoseValue.glucose)
+                        : Decimal(firstGlucoseValue.glucose).asMmolL
+
+                    self.state.glucose = self.glucoseFormatter.string(from: value as NSNumber)
+                    self.state.trend = firstGlucoseValue.directionEnum?.symbol
+
+                    let delta = glucoseValues.count >= 2
+                        ? Decimal(firstGlucoseValue.glucose) - Decimal(glucoseValues.dropFirst().first?.glucose ?? 0)
+                        : 0
+                    let deltaConverted = self.settingsManager.settings.units == .mgdL ? delta : delta.asMmolL
+                    self.state.delta = self.deltaFormatter.string(from: deltaConverted as NSNumber)
+                    self.state.trendRaw = firstGlucoseValue.direction
+                    self.state.glucoseDate = firstGlucoseValue.date
+                }
 
-            if !(state.useNewCalc ?? false) {
-                state.bolusRecommended = apsManager
-                    .roundBolus(amount: max(
-                        insulinRequired * (settingsManager.settings.insulinReqPercentage / 100) * double,
-                        0
-                    ))
-            } else {
-                let recommended = await newBolusCalc(
-                    ids: glucoseValuesIDs,
-                    determination: lastDetermination
-                )
-                state.bolusRecommended = apsManager
-                    .roundBolus(amount: max(recommended, 0))
-            }
-            state.bolusAfterCarbs = !settingsManager.settings.skipBolusScreenAfterCarbs
-            state.displayOnWatch = settingsManager.settings.displayOnWatch
-            state.displayFatAndProteinOnWatch = settingsManager.settings.displayFatAndProteinOnWatch
-            state.confirmBolusFaster = settingsManager.settings.confirmBolusFaster
-
-            state.iob = lastDetermination?.iob as? Decimal
-            state.cob = lastDetermination?.cob as? Decimal
-            state.tempTargets = tempTargetsStorage.presets()
-                .map { target -> TempTargetWatchPreset in
-                    let untilDate = self.tempTargetsStorage.current().flatMap { currentTarget -> Date? in
-                        guard currentTarget.id == target.id else { return nil }
-                        let date = currentTarget.createdAt.addingTimeInterval(TimeInterval(currentTarget.duration * 60))
-                        return date > Date() ? date : nil
+                self.state.lastLoopDate = lastDetermination?.timestamp
+                self.state.lastLoopDateInterval = self.state.lastLoopDate.map {
+                    guard $0.timeIntervalSince1970 > 0 else { return 0 }
+                    return UInt64($0.timeIntervalSince1970)
+                }
+                self.state.bolusIncrement = self.settingsManager.preferences.bolusIncrement
+                self.state.maxCOB = self.settingsManager.preferences.maxCOB
+                self.state.maxBolus = self.settingsManager.pumpSettings.maxBolus
+                self.state.carbsRequired = lastDetermination?.carbsRequired as? Decimal
+
+//                var insulinRequired = lastDetermination?.insulinReq as? Decimal ?? 0
+//
+//                var double: Decimal = 2
+//                if lastDetermination?.manualBolusErrorString == 0 {
+//                    insulinRequired = lastDetermination?.insulinForManualBolus as? Decimal ?? 0
+//                    double = 1
+//                }
+
+                self.state.useNewCalc = self.settingsManager.settings.useCalc
+                self.state.bolusRecommended = self.apsManager
+                    .roundBolus(amount: max(recommendedInsulin, 0))
+                self.state.bolusAfterCarbs = !self.settingsManager.settings.skipBolusScreenAfterCarbs
+                self.state.displayOnWatch = self.settingsManager.settings.displayOnWatch
+                self.state.displayFatAndProteinOnWatch = self.settingsManager.settings.displayFatAndProteinOnWatch
+                self.state.confirmBolusFaster = self.settingsManager.settings.confirmBolusFaster
+
+                self.state.iob = lastDetermination?.iob as? Decimal
+                if let cobValue = lastDetermination?.cob {
+                    self.state.cob = Decimal(cobValue)
+                } else {
+                    self.state.cob = 0
+                }
+                self.state.tempTargets = self.tempTargetsStorage.presets()
+                    .map { target -> TempTargetWatchPreset in
+                        let untilDate = self.tempTargetsStorage.current().flatMap { currentTarget -> Date? in
+                            guard currentTarget.id == target.id else { return nil }
+                            let date = currentTarget.createdAt.addingTimeInterval(TimeInterval(currentTarget.duration * 60))
+                            return date > Date() ? date : nil
+                        }
+                        return TempTargetWatchPreset(
+                            name: target.displayName,
+                            id: target.id,
+                            description: self.descriptionForTarget(target),
+                            until: untilDate
+                        )
                     }
-                    return TempTargetWatchPreset(
-                        name: target.displayName,
-                        id: target.id,
-                        description: self.descriptionForTarget(target),
-                        until: untilDate
-                    )
+                self.state.bolusAfterCarbs = !self.settingsManager.settings.skipBolusScreenAfterCarbs
+                self.state.displayOnWatch = self.settingsManager.settings.displayOnWatch
+                self.state.displayFatAndProteinOnWatch = self.settingsManager.settings.displayFatAndProteinOnWatch
+                self.state.confirmBolusFaster = self.settingsManager.settings.confirmBolusFaster
+
+                if let eventualBG = self.settingsManager.settings.units == .mgdL ? lastDetermination?
+                    .eventualBG : lastDetermination?
+                    .eventualBG?.decimalValue.asMmolL as NSDecimalNumber?
+                {
+                    let eventualBGAsString = self.eventualFormatter.string(from: eventualBG)
+                    self.state.eventualBG = eventualBGAsString.map { "⇢ " + $0 }
+                    self.state.eventualBGRaw = eventualBGAsString
                 }
-            state.bolusAfterCarbs = !settingsManager.settings.skipBolusScreenAfterCarbs
-            state.displayOnWatch = settingsManager.settings.displayOnWatch
-            state.displayFatAndProteinOnWatch = settingsManager.settings.displayFatAndProteinOnWatch
-            state.confirmBolusFaster = settingsManager.settings.confirmBolusFaster
-
-            if let eventualBG = settingsManager.settings.units == .mgdL ? lastDetermination?.eventualBG : lastDetermination?
-                .eventualBG?.decimalValue.asMmolL as NSDecimalNumber?
-            {
-                let eventualBGAsString = eventualFormatter.string(from: eventualBG)
-                state.eventualBG = eventualBGAsString.map { "⇢ " + $0 }
-                state.eventualBGRaw = eventualBGAsString
-            }
 
-            state.isf = lastDetermination?.insulinSensitivity as? Decimal
+                self.state.isf = lastDetermination?.insulinSensitivity as? Decimal
 
-            if latestOverride?.enabled ?? false {
-                let percentString = "\((latestOverride?.percentage ?? 100).formatted(.number)) %"
-                state.override = percentString
+                if latestOverride?.enabled ?? false {
+                    let percentString = "\((latestOverride?.percentage ?? 100).formatted(.number)) %"
+                    self.state.override = percentString
 
-            } else {
-                state.override = "100 %"
-            }
+                } else {
+                    self.state.override = "100 %"
+                }
 
-            sendState()
+                self.sendState()
+            }
 
         } catch let error as NSError {
             debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to configure state with error: \(error)")

+ 28 - 2
FreeAPS/Sources/Views/TextFieldWithToolBar.swift

@@ -15,6 +15,8 @@ public struct TextFieldWithToolBar: UIViewRepresentable {
     var textFieldDidBeginEditing: (() -> Void)?
     var numberFormatter: NumberFormatter
     var allowDecimalSeparator: Bool
+    var previousTextField: (() -> Void)?
+    var nextTextField: (() -> Void)?
 
     public init(
         text: Binding<Decimal>,
@@ -29,7 +31,9 @@ public struct TextFieldWithToolBar: UIViewRepresentable {
         isDismissible: Bool = true,
         textFieldDidBeginEditing: (() -> Void)? = nil,
         numberFormatter: NumberFormatter,
-        allowDecimalSeparator: Bool = true
+        allowDecimalSeparator: Bool = true,
+        previousTextField: (() -> Void)? = nil,
+        nextTextField: (() -> Void)? = nil
     ) {
         _text = text
         self.placeholder = placeholder
@@ -45,6 +49,8 @@ public struct TextFieldWithToolBar: UIViewRepresentable {
         self.numberFormatter = numberFormatter
         self.numberFormatter.numberStyle = .decimal
         self.allowDecimalSeparator = allowDecimalSeparator
+        self.previousTextField = previousTextField
+        self.nextTextField = nextTextField
     }
 
     public func makeUIView(context: Context) -> UITextField {
@@ -77,8 +83,20 @@ public struct TextFieldWithToolBar: UIViewRepresentable {
             target: context.coordinator,
             action: #selector(Coordinator.clearText)
         )
+        let previousButton = UIBarButtonItem(
+            image: UIImage(systemName: "chevron.up"),
+            style: .plain,
+            target: context.coordinator,
+            action: #selector(Coordinator.previousTextField)
+        )
+        let nextButton = UIBarButtonItem(
+            image: UIImage(systemName: "chevron.down"),
+            style: .plain,
+            target: context.coordinator,
+            action: #selector(Coordinator.nextTextField)
+        )
 
-        toolbar.items = [clearButton, flexibleSpace, doneButton]
+        toolbar.items = [clearButton, previousButton, nextButton, flexibleSpace, doneButton]
         toolbar.sizeToFit()
         return toolbar
     }
@@ -136,6 +154,14 @@ public struct TextFieldWithToolBar: UIViewRepresentable {
             }
         }
 
+        @objc fileprivate func previousTextField() {
+            parent.previousTextField?()
+        }
+
+        @objc fileprivate func nextTextField() {
+            parent.nextTextField?()
+        }
+
         // Helper method to calculate the number of decimal places in a string
         fileprivate func calculateDecimalPlaces(in string: String) -> Int {
             guard let decimalSeparator = decimalFormatter.decimalSeparator else { return 0 }

+ 62 - 9
LiveActivity/LiveActivity.swift

@@ -1,5 +1,6 @@
 import ActivityKit
 import Charts
+import Foundation
 import SwiftUI
 import WidgetKit
 
@@ -9,6 +10,55 @@ private enum Size {
     case expanded
 }
 
+enum GlucoseUnits: String, Equatable {
+    case mgdL = "mg/dL"
+    case mmolL = "mmol/L"
+
+    static let exchangeRate: Decimal = 0.0555
+}
+
+func rounded(_ value: Decimal, scale: Int, roundingMode: NSDecimalNumber.RoundingMode) -> Decimal {
+    var result = Decimal()
+    var toRound = value
+    NSDecimalRound(&result, &toRound, scale, roundingMode)
+    return result
+}
+
+extension Int {
+    var asMmolL: Decimal {
+        rounded(Decimal(self) * GlucoseUnits.exchangeRate, scale: 1, roundingMode: .plain)
+    }
+
+    var formattedAsMmolL: String {
+        NumberFormatter.glucoseFormatter.string(from: asMmolL as NSDecimalNumber) ?? "\(asMmolL)"
+    }
+}
+
+extension Decimal {
+    var asMmolL: Decimal {
+        rounded(self * GlucoseUnits.exchangeRate, scale: 1, roundingMode: .plain)
+    }
+
+    var asMgdL: Decimal {
+        rounded(self / GlucoseUnits.exchangeRate, scale: 0, roundingMode: .plain)
+    }
+
+    var formattedAsMmolL: String {
+        NumberFormatter.glucoseFormatter.string(from: asMmolL as NSDecimalNumber) ?? "\(asMmolL)"
+    }
+}
+
+extension NumberFormatter {
+    static let glucoseFormatter: NumberFormatter = {
+        let formatter = NumberFormatter()
+        formatter.locale = Locale.current
+        formatter.numberStyle = .decimal
+        formatter.minimumFractionDigits = 1
+        formatter.maximumFractionDigits = 1
+        return formatter
+    }()
+}
+
 struct LiveActivity: Widget {
     private let dateFormatter: DateFormatter = {
         var f = DateFormatter()
@@ -215,26 +265,29 @@ struct LiveActivity: Widget {
             Text("No data available")
         } else {
             // Determine scale
-            let conversionFactor = additionalState.unit == "mmol/L" ? 0.0555 : 1
-            let min = (additionalState.chart.min() ?? 40 * conversionFactor) - 20 * conversionFactor
-            let max = (additionalState.chart.max() ?? 270 * conversionFactor) + 50 * conversionFactor
+            let min = min(additionalState.chart.min() ?? 45, 40) - 20
+            let max = max(additionalState.chart.max() ?? 270, 300) + 50
+
+            let yAxisRuleMarkMin = additionalState.unit == "mg/dL" ? additionalState.lowGlucose : additionalState.lowGlucose
+                .asMmolL
+            let yAxisRuleMarkMax = additionalState.unit == "mg/dL" ? additionalState.highGlucose : additionalState.highGlucose
+                .asMmolL
 
             Chart {
-                RuleMark(y: .value("High", additionalState.highGlucose))
+                RuleMark(y: .value("Low", yAxisRuleMarkMin))
                     .lineStyle(.init(lineWidth: 0.5, dash: [5]))
-                RuleMark(y: .value("Low", additionalState.lowGlucose))
+                RuleMark(y: .value("High", yAxisRuleMarkMax))
                     .lineStyle(.init(lineWidth: 0.5, dash: [5]))
 
                 ForEach(additionalState.chart.indices, id: \.self) { index in
                     let currentValue = additionalState.chart[index]
+                    let displayValue = additionalState.unit == "mg/dL" ? currentValue : currentValue.asMmolL
                     let chartDate = additionalState.chartDate[index] ?? Date()
                     let pointMark = PointMark(
                         x: .value("Time", chartDate),
-                        y: .value("Value", currentValue)
+                        y: .value("Value", displayValue)
                     ).symbolSize(15)
 
-                    // let color = setBGColor(bgValue: Int(currentValue), highBGColorValue: additionalState.highGlucose, lowBGColorValue: additionalState.lowGlucose, dynamicBGColor: additionalState.dynamicBGColor)
-
                     let color = setBGColor(
                         bgValue: Int(currentValue),
                         highBGColorValue: Decimal(additionalState.highGlucose),
@@ -251,7 +304,7 @@ struct LiveActivity: Widget {
                     AxisValueLabel().foregroundStyle(.secondary).font(.footnote)
                 }
             }
-            .chartYScale(domain: min ... max)
+            .chartYScale(domain: additionalState.unit == "mg/dL" ? min ... max : min.asMmolL ... max.asMmolL)
             .chartXAxis {
                 AxisMarks(position: .automatic) { _ in
                     AxisGridLine(stroke: .init(lineWidth: 0.2, dash: [2, 3])).foregroundStyle(Color.white)

+ 44 - 20
Model/CoreDataStack.swift

@@ -106,7 +106,7 @@ class CoreDataStack: ObservableObject {
     private func fetchPersistentHistoryTransactionsAndChanges() async throws {
         let taskContext = newTaskContext()
         taskContext.name = "persistentHistoryContext"
-        debugPrint("Start fetching persistent history changes from the store ... \(DebuggingIdentifiers.inProgress)")
+//        debugPrint("Start fetching persistent history changes from the store ... \(DebuggingIdentifiers.inProgress)")
 
         try await taskContext.perform {
             // Execute the persistent history change since the last transaction
@@ -121,7 +121,7 @@ class CoreDataStack: ObservableObject {
     }
 
     private func mergePersistentHistoryChanges(from history: [NSPersistentHistoryTransaction]) {
-        debugPrint("Received \(history.count) persistent history transactions")
+//        debugPrint("Received \(history.count) persistent history transactions")
         // Update view context with objectIDs from history change request
         /// - Tag: mergeChanges
         let viewContext = persistentContainer.viewContext
@@ -210,7 +210,7 @@ extension CoreDataStack {
 
             // Guard check if there are NSManagedObjects older than the specified days
             guard !objectIDs.isEmpty else {
-                debugPrint("No objects found older than \(days) days.")
+//                debugPrint("No objects found older than \(days) days.")
                 return
             }
 
@@ -258,7 +258,7 @@ extension CoreDataStack {
             }
 
             guard !parentObjectIDs.isEmpty else {
-                debugPrint("No \(parentType) objects found older than \(days) days.")
+//                debugPrint("No \(parentType) objects found older than \(days) days.")
                 return
             }
 
@@ -272,7 +272,7 @@ extension CoreDataStack {
             }
 
             guard !childObjectIDs.isEmpty else {
-                debugPrint("No \(childType) objects found related to \(parentType) objects older than \(days) days.")
+//                debugPrint("No \(childType) objects found related to \(parentType) objects older than \(days) days.")
                 return
             }
 
@@ -339,9 +339,9 @@ extension CoreDataStack {
         /// we need to ensure that the fetch immediately returns a value as long as the whole app does not use the async await pattern, otherwise we could perform this asynchronously with backgroundContext.perform and not block the thread
         context.performAndWait {
             do {
-                debugPrint(
-                    "Fetching \(T.self) in \(callingFunction) from \(callingClass): \(DebuggingIdentifiers.succeeded) on Thread: \(Thread.current)"
-                )
+//                debugPrint(
+//                    "Fetching \(T.self) in \(callingFunction) from \(callingClass): \(DebuggingIdentifiers.succeeded) on Thread: \(Thread.current)"
+//                )
                 result = try context.fetch(request)
             } catch let error as NSError {
                 debugPrint(
@@ -365,8 +365,8 @@ extension CoreDataStack {
         propertiesToFetch: [String]? = nil,
         callingFunction: String = #function,
         callingClass: String = #fileID
-    ) async -> [T] {
-        let request = NSFetchRequest<T>(entityName: String(describing: type))
+    ) async -> Any {
+        let request: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: String(describing: type))
         request.sortDescriptors = [NSSortDescriptor(key: key, ascending: ascending)]
         request.predicate = predicate
         if let limit = fetchLimit {
@@ -375,9 +375,9 @@ extension CoreDataStack {
         if let batchSize = batchSize {
             request.fetchBatchSize = batchSize
         }
-        if let propertiesTofetch = propertiesToFetch {
-            request.propertiesToFetch = propertiesTofetch
-            request.resultType = .managedObjectResultType
+        if let propertiesToFetch = propertiesToFetch {
+            request.propertiesToFetch = propertiesToFetch
+            request.resultType = .dictionaryResultType
         } else {
             request.resultType = .managedObjectResultType
         }
@@ -387,10 +387,14 @@ extension CoreDataStack {
 
         return await context.perform {
             do {
-                debugPrint(
-                    "Fetching \(T.self) in \(callingFunction) from \(callingClass): \(DebuggingIdentifiers.succeeded) on Thread: \(Thread.current)"
-                )
-                return try context.fetch(request)
+//                debugPrint(
+//                    "Fetching \(T.self) in \(callingFunction) from \(callingClass): \(DebuggingIdentifiers.succeeded) on Thread: \(Thread.current)"
+//                )
+                if propertiesToFetch != nil {
+                    return try context.fetch(request) as? [[String: Any]] ?? []
+                } else {
+                    return try context.fetch(request) as? [T] ?? []
+                }
             } catch let error as NSError {
                 debugPrint(
                     "Fetching \(T.self) in \(callingFunction) from \(callingClass): \(DebuggingIdentifiers.failed) \(error) on Thread: \(Thread.current)"
@@ -399,6 +403,26 @@ extension CoreDataStack {
             }
         }
     }
+
+    // Get NSManagedObject
+    func getNSManagedObject<T: NSManagedObject>(
+        with ids: [NSManagedObjectID],
+        context: NSManagedObjectContext
+    ) async -> [T] {
+        await Task { () -> [T] in
+            var objects = [T]()
+            do {
+                for id in ids {
+                    if let object = try context.existingObject(with: id) as? T {
+                        objects.append(object)
+                    }
+                }
+            } catch {
+                debugPrint("Failed to fetch objects: \(error.localizedDescription)")
+            }
+            return objects
+        }.value
+    }
 }
 
 // MARK: - Save
@@ -429,9 +453,9 @@ extension NSManagedObjectContext {
         do {
             guard onContext.hasChanges else { return }
             try onContext.save()
-            debugPrint(
-                "Saving to Core Data successful in \(callingFunction) in \(callingClass): \(DebuggingIdentifiers.succeeded)"
-            )
+//            debugPrint(
+//                "Saving to Core Data successful in \(callingFunction) in \(callingClass): \(DebuggingIdentifiers.succeeded)"
+//            )
         } catch let error as NSError {
             debugPrint(
                 "Saving to Core Data failed in \(callingFunction) in \(callingClass): \(DebuggingIdentifiers.failed) with error \(error), \(error.userInfo)"

+ 17 - 0
Model/Helper/CarbsGlucose+helper.swift

@@ -0,0 +1,17 @@
+import Foundation
+
+struct CarbAndGlucose: Encodable {
+    let carbs: Decimal
+    let glucose: Decimal
+
+    enum CodingKeys: String, CodingKey {
+        case carbs
+        case glucose
+    }
+
+    func encode(to encoder: Encoder) throws {
+        var container = encoder.container(keyedBy: CodingKeys.self)
+        try container.encode(NSDecimalNumber(decimal: carbs).stringValue, forKey: .carbs)
+        try container.encode(NSDecimalNumber(decimal: glucose).stringValue, forKey: .glucose)
+    }
+}

+ 18 - 0
Model/Helper/NSPredicates.swift

@@ -6,6 +6,10 @@ extension Date {
         Calendar.current.startOfDay(for: Date())
     }
 
+    static var oneDayAgoInMinutes: Date {
+        Calendar.current.date(byAdding: .minute, value: -1440, to: Date())!
+    }
+
     static var oneDayAgo: Date {
         Calendar.current.date(byAdding: .day, value: -1, to: Date())!
     }
@@ -26,6 +30,10 @@ extension Date {
         Calendar.current.date(byAdding: .hour, value: -2, to: Date())!
     }
 
+    static var fourHoursAgo: Date {
+        Calendar.current.date(byAdding: .hour, value: -4, to: Date())!
+    }
+
     static var sixHoursAgo: Date {
         Calendar.current.date(byAdding: .hour, value: -6, to: Date())!
     }
@@ -48,6 +56,11 @@ extension NSPredicate {
 
     static let none = NSPredicate(format: "FALSEPREDICATE")
 
+    static var predicateForOneDayAgoInMinutes: NSPredicate {
+        let date = Date.oneDayAgoInMinutes
+        return NSPredicate(format: "date >= %@", date as NSDate)
+    }
+
     static var predicateForOneDayAgo: NSPredicate {
         let date = Date.oneDayAgo
         return NSPredicate(format: "date >= %@", date as NSDate)
@@ -78,6 +91,11 @@ extension NSPredicate {
         return NSPredicate(format: "date >= %@", date as NSDate)
     }
 
+    static var predicateForFourHoursAgo: NSPredicate {
+        let date = Date.fourHoursAgo
+        return NSPredicate(format: "date >= %@", date as NSDate)
+    }
+
     static var predicateForSixHoursAgo: NSPredicate {
         let date = Date.sixHoursAgo
         return NSPredicate(format: "date >= %@", date as NSDate)

+ 8 - 3
Model/Helper/PumpEvent+helper.swift

@@ -53,6 +53,11 @@ public extension PumpEventStored {
 }
 
 extension NSPredicate {
+    static var pumpHistoryLast1440Minutes: NSPredicate {
+        let date = Date.oneDayAgoInMinutes
+        return NSPredicate(format: "timestamp >= %@", date as NSDate)
+    }
+
     static var pumpHistoryLast24h: NSPredicate {
         let date = Date.oneDayAgo
         return NSPredicate(format: "timestamp >= %@", date as NSDate)
@@ -155,7 +160,7 @@ extension PumpEventStored {
     }
 
     func toTempBasalDTOEnum() -> PumpEventDTO? {
-        guard let timestamp = timestamp, let tempBasal = tempBasal, let rate = tempBasal.rate else {
+        guard let id = id, let timestamp = timestamp, let tempBasal = tempBasal, let rate = tempBasal.rate else {
             return nil
         }
 
@@ -169,12 +174,12 @@ extension PumpEventStored {
     }
 
     func toTempBasalDurationDTOEnum() -> PumpEventDTO? {
-        guard let timestamp = timestamp, let tempBasal = tempBasal else {
+        guard let id = id, let timestamp = timestamp, let tempBasal = tempBasal else {
             return nil
         }
 
         let tempBasalDurationDTO = TempBasalDurationDTO(
-            id: id ?? UUID().uuidString,
+            id: id,
             timestamp: PumpEventStored.dateFormatter.string(from: timestamp),
             duration: Int(tempBasal.duration)
         )

+ 3 - 2
Trio.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -1,4 +1,5 @@
 {
+  "originHash" : "59ac7eba66375d6eb406e758cb0b9964f4b3b0ae45c5665596f00384c32262b9",
   "pins" : [
     {
       "identity" : "cryptoswift",
@@ -48,7 +49,7 @@
     {
       "identity" : "swiftcharts",
       "kind" : "remoteSourceControl",
-      "location" : "https://github.com/ivanschuetz/SwiftCharts",
+      "location" : "https://github.com/ivanschuetz/SwiftCharts.git",
       "state" : {
         "branch" : "master",
         "revision" : "c354c1945bb35a1f01b665b22474f6db28cba4a2"
@@ -91,5 +92,5 @@
       }
     }
   ],
-  "version" : 2
+  "version" : 3
 }

+ 4 - 1
oref0_source_version.txt

@@ -1,6 +1,9 @@
-oref0 branch: dev - git version: d1dfb70
+oref0 branch: dev - git version: 363fd11
 
 Last commits:
+363fd11 Merge pull request #28 from bjornoleh/harmonise_defaults
+2d695e1 index.js: set enableUAM to false, and remove whitespace in L11
+8f5f820 Harmonise profile defaults with openaps/oref0
 d1dfb70 Merge pull request #26 from MikePlante1/typo
 d9f1662 fix `threshold_setting` typo
 b454837 Merge pull request #24 from nightscout/Trio_renames

+ 1 - 6
scripts/capture-build-details.sh

@@ -1,22 +1,17 @@
 #!/bin/sh -e
-
 #  capture-build-details.sh
 #  Trio
 #
 #  Created by Jonas Björkert on 2024-05-08.
-
 # Enable debugging if needed
 #set -x
-
 info_plist_path="${BUILT_PRODUCTS_DIR}/${CONTENTS_FOLDER_PATH}/BuildDetails.plist"
-
 # Ensure the path to BuildDetails.plist is valid.
 if [ "${info_plist_path}" == "/" -o ! -e "${info_plist_path}" ]; then
     echo "BuildDetails.plist file does not exist at path: ${info_plist_path}" >&2
     exit 1
 else
     echo "Gathering build details..."
-
     # Capture the current date and write it to BuildDetails.plist
     plutil -replace com-trio-build-date -string "$(date)" "${info_plist_path}"
 
@@ -40,4 +35,4 @@ else
 
     # Update BuildDetails.plist with the SHA information
     plutil -replace com-trio-commit-sha -string "${git_commit_sha}" "${info_plist_path}"
-fi
+fi

+ 15 - 16
trio-oref/lib/profile/index.js

@@ -8,11 +8,11 @@ var _ = require('lodash');
 
 function defaults ( ) {
   return /* profile */ {
-    max_iob: 9 // if max_iob is not provided, will default to zero
-    , max_daily_safety_multiplier: 5
-    , current_basal_safety_multiplier: 6
-    , autosens_max: 2.5
-    , autosens_min: 0.5
+    max_iob: 0 // if max_iob is not provided, will default to zero
+    , max_daily_safety_multiplier: 3
+    , current_basal_safety_multiplier: 4
+    , autosens_max: 1.2
+    , autosens_min: 0.7
     , rewind_resets_autosens: true // reset autosensitivity to neutral for awhile after each pump rewind
     // , autosens_adjust_targets: false // when autosens detects sensitivity/resistance, also adjust BG target accordingly
     , high_temptarget_raises_sensitivity: false // raise sensitivity for temptargets >= 101.  synonym for exercise_mode
@@ -32,10 +32,10 @@ function defaults ( ) {
     , remainingCarbsFraction: 1.0 // fraction of carbs we'll assume will absorb over 4h if we don't yet see carb absorption
     , remainingCarbsCap: 90 // max carbs we'll assume will absorb over 4h if we don't yet see carb absorption
     // WARNING: use SMB with caution: it can and will automatically bolus up to max_iob worth of extra insulin
-    , enableUAM: true // enable detection of unannounced meal carb absorption
+    , enableUAM: false // enable detection of unannounced meal carb absorption
     , A52_risk_enable: false
-    , enableSMB_with_COB: true // enable supermicrobolus while COB is positive
-    , enableSMB_with_temptarget: true // enable supermicrobolus for eating soon temp targets
+    , enableSMB_with_COB: false // enable supermicrobolus while COB is positive
+    , enableSMB_with_temptarget: false // enable supermicrobolus for eating soon temp targets
     // *** WARNING *** DO NOT USE enableSMB_always or enableSMB_after_carbs with Libre or similar
     // LimiTTer, etc. do not properly filter out high-noise SGVs.  xDrip+ builds greater than or equal to
     // version number d8e-7097-2018-01-22 provide proper noise values, so that oref0 can ignore high noise
@@ -45,16 +45,18 @@ function defaults ( ) {
     // if the CGM sensor reads falsely high and doesn't come down as actual BG does
     , enableSMB_always: false // always enable supermicrobolus (unless disabled by high temptarget)
     , enableSMB_after_carbs: false // enable supermicrobolus for 6h after carbs, even with 0 COB
+    , 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.
     // *** WARNING *** DO NOT USE enableSMB_always or enableSMB_after_carbs with Libre or similar.
-    , allowSMB_with_high_temptarget: true // allow supermicrobolus (if otherwise enabled) even with high temp targets
-    , maxSMBBasalMinutes: 90 // maximum minutes of basal that can be delivered as a single SMB with uncovered COB
-    , maxUAMSMBBasalMinutes: 90 // maximum minutes of basal that can be delivered as a single SMB when IOB exceeds COB
+    , allowSMB_with_high_temptarget: false // allow supermicrobolus (if otherwise enabled) even with high temp targets
+    , maxSMBBasalMinutes: 30 // maximum minutes of basal that can be delivered as a single SMB with uncovered COB
+    , maxUAMSMBBasalMinutes: 30 // maximum minutes of basal that can be delivered as a single SMB when IOB exceeds COB
     , SMBInterval: 3 // minimum interval between SMBs, in minutes.
-    , bolus_increment: 0.05 // minimum bolus that can be delivered as an SMB
+    , bolus_increment: 0.1 // minimum bolus that can be delivered as an SMB
     , maxDelta_bg_threshold: 0.2 // maximum change in bg to use SMB, above that will disable SMB
     , curve: "rapid-acting" // change this to "ultra-rapid" for Fiasp, or "bilinear" for old curve
     , useCustomPeakTime: false // allows changing insulinPeakTime
-    , insulinPeakTime: 45 // number of minutes after a bolus activity peaks.  defaults to 55m for Fiasp if useCustomPeakTime: false
+    , insulinPeakTime: 75 // number of minutes after a bolus activity peaks.  defaults to 55m for Fiasp if useCustomPeakTime: false
     , carbsReqThreshold: 1 // grams of carbsReq to trigger a pushover
     , offline_hotspot: false // enabled an offline-only local wifi hotspot if no Internet available
     , noisyCGMTargetMultiplier: 1.3 // increase target by this amount when looping off raw/noisy CGM data
@@ -66,7 +68,6 @@ function defaults ( ) {
     //, maxRaw: 200 // highest raw/noisy CGM value considered safe to use for looping
     , calc_glucose_noise: false
     , target_bg: false // set to an integer value in mg/dL to override pump min_bg
-    // autoISF variables
     , smb_delivery_ratio: 0.5 //Default value: 0.5 Used if flexible delivery ratio is not used. This is another key OpenAPS safety cap, and specifies what share of the total insulin required can be delivered as SMB. This is to prevent people from getting into dangerous territory by setting SMB requests from the caregivers phone at the same time. Increase this experimental value slowly and with caution.
     , adjustmentFactor: 0.8
     , adjustmentFactorSigmoid: 0.5
@@ -75,8 +76,6 @@ function defaults ( ) {
     , sigmoid: false
     , weightPercentage: 0.65 
     , tddAdjBasal: false // Enable adjustment of basal based on the ratio of 24 h : 10 day average TDD
-    , enableSMB_high_bg: false // enable SMBs when a high BG is detected, based on the high BG target (adjusted or profile)
-    , enableSMB_high_bg_target: 110 // set the value enableSMB_high_bg will compare against to enable SMB. If BG > than this value, SMBs should enable.
     , threshold_setting: 60 // Use a configurable threshold setting
   }
 }