Bläddra i källkod

Merge pull request #30 from polscm32/forecast-chart

Forecast chart
polscm32 1 år sedan
förälder
incheckning
3246be1543
46 ändrade filer med 2404 tillägg och 1095 borttagningar
  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. 31 8
      FreeAPS/Sources/APS/APSManager.swift
  13. 103 37
      FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift
  14. 2 0
      FreeAPS/Sources/APS/Storage/CarbsStorage.swift
  15. 19 10
      FreeAPS/Sources/APS/Storage/DeterminationStorage.swift
  16. 6 0
      FreeAPS/Sources/APS/Storage/GlucoseStorage.swift
  17. 8 0
      FreeAPS/Sources/Helpers/Rounding.swift
  18. 28 5
      FreeAPS/Sources/Models/BloodGlucose.swift
  19. 5 0
      FreeAPS/Sources/Models/FreeAPSSettings.swift
  20. 8 8
      FreeAPS/Sources/Modules/AutotuneConfig/View/AutotuneConfigRootView.swift
  21. 5 1
      FreeAPS/Sources/Modules/Bolus/BolusDataFlow.swift
  22. 26 4
      FreeAPS/Sources/Modules/Bolus/BolusProvider.swift
  23. 278 183
      FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift
  24. 119 0
      FreeAPS/Sources/Modules/Bolus/View/AddMealPresetView.swift
  25. 86 207
      FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift
  26. 255 0
      FreeAPS/Sources/Modules/Bolus/View/ForeCastChart.swift
  27. 333 0
      FreeAPS/Sources/Modules/Bolus/View/MealPresetView.swift
  28. 5 0
      FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift
  29. 28 17
      FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift
  30. 181 159
      FreeAPS/Sources/Modules/Home/HomeStateModel.swift
  31. 308 89
      FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift
  32. 9 14
      FreeAPS/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift
  33. 36 24
      FreeAPS/Sources/Modules/Home/View/HomeRootView.swift
  34. 3 14
      FreeAPS/Sources/Modules/OverrideConfig/View/OverrideRootView.swift
  35. 2 0
      FreeAPS/Sources/Modules/StatConfig/StatConfigStateModel.swift
  36. 1 0
      FreeAPS/Sources/Modules/StatConfig/View/StatConfigRootView.swift
  37. 3 3
      FreeAPS/Sources/Services/LiveActivity/LiveActitiyAttributes.swift
  38. 8 20
      FreeAPS/Sources/Services/LiveActivity/LiveActivityAttributes+Helper.swift
  39. 103 95
      FreeAPS/Sources/Services/WatchManager/WatchManager.swift
  40. 28 2
      FreeAPS/Sources/Views/TextFieldWithToolBar.swift
  41. 64 9
      LiveActivity/LiveActivity.swift
  42. 20 0
      Model/CoreDataStack.swift
  43. 17 0
      Model/Helper/CarbsGlucose+helper.swift
  44. 9 0
      Model/Helper/NSPredicates.swift
  45. 4 1
      oref0_source_version.txt
  46. 15 16
      trio-oref/lib/profile/index.js

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

@@ -10,12 +10,13 @@ jobs:
     secrets: inherit
     secrets: inherit
 
 
   identifiers:
   identifiers:
+    name: Add Identifiers
     needs: validate
     needs: validate
-    runs-on: macos-13
+    runs-on: macos-14
     steps:
     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
       # Checks-out the repo
       - name: Checkout Repo
       - name: Checkout Repo
@@ -23,12 +24,23 @@ jobs:
 
 
       # Patch Fastlane Match to not print tables
       # Patch Fastlane Match to not print tables
       - name: Patch Match 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
       # Install project dependencies
       - name: Install Project Dependencies
       - name: Install Project Dependencies
         run: bundle install
         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
       # Create or update identifiers for app
       - name: Fastlane Provision
       - name: Fastlane Provision
         run: bundle exec fastlane identifiers
         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 }})
 run-name: Build Trio (${{ github.ref_name }})
 on:
 on:
   workflow_dispatch:
   workflow_dispatch:
-  
+
   ## Remove the "#" sign from the beginning of the line below to get automated builds on push (code changes in your repository)
   ## Remove the "#" sign from the beginning of the line below to get automated builds on push (code changes in your repository)
   #push:
   #push:
-  
+
   schedule:
   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:  
 env:  
   UPSTREAM_REPO: nightscout/Trio
   UPSTREAM_REPO: nightscout/Trio
   UPSTREAM_BRANCH: ${{ github.ref_name }} # branch on upstream repository to sync from (replace with specific branch name if needed)
   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)
   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:
 jobs:
   validate:
   validate:
@@ -33,151 +33,172 @@ jobs:
       contents: write
       contents: write
     outputs:
     outputs:
       WORKFLOW_PERMISSION: ${{ steps.workflow-permission.outputs.has_permission }}
       WORKFLOW_PERMISSION: ${{ steps.workflow-permission.outputs.has_permission }}
-    
+
     steps:
     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
   # Checks for changes in upstream repository; if changes exist prompts sync for build
   # Performs keepalive to avoid stale fork
   # Performs keepalive to avoid stale fork
   check_latest_from_upstream:
   check_latest_from_upstream:
     needs: [validate, check_alive_and_permissions]
     needs: [validate, check_alive_and_permissions]
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     name: Check upstream and keep alive
     name: Check upstream and keep alive
-    outputs: 
+    outputs:
       NEW_COMMITS: ${{ steps.sync.outputs.has_new_commits }}
       NEW_COMMITS: ${{ steps.sync.outputs.has_new_commits }}
-    
+      ABORT_SYNC: ${{ steps.check_branch.outputs.ABORT_SYNC }}
+
     steps:
     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
               under the following path <code>Trio/fastlane/testflight.md</code>." >> $GITHUB_STEP_SUMMARY
-   
   
   
   # Builds Trio
   # Builds Trio
   build:
   build:
     name: Build
     name: Build
     needs: [validate, check_alive_and_permissions, check_latest_from_upstream]
     needs: [validate, check_alive_and_permissions, check_latest_from_upstream]
-    runs-on: macos-13
+    runs-on: macos-14
     permissions:
     permissions:
       contents: write
       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:
     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
       - name: Checkout Repo for syncing
         if: |
         if: |
@@ -186,12 +207,12 @@ jobs:
         uses: actions/checkout@v4
         uses: actions/checkout@v4
         with:
         with:
           token: ${{ secrets.GH_PAT }}
           token: ${{ secrets.GH_PAT }}
-          ref: ${{ env.TARGET_BRANCH }} 
-      
+          ref: ${{ env.TARGET_BRANCH }}
+
       - name: Sync upstream changes
       - name: Sync upstream changes
         if: | # do not run the upstream sync action on the upstream repository
         if: | # do not run the upstream sync action on the upstream repository
           needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' &&
           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
         id: sync
         uses: aormsby/Fork-Sync-With-Upstream-action@v3.4.1
         uses: aormsby/Fork-Sync-With-Upstream-action@v3.4.1
         with:
         with:
@@ -200,24 +221,24 @@ jobs:
           target_repo_token: ${{ secrets.GH_PAT }}
           target_repo_token: ${{ secrets.GH_PAT }}
           upstream_sync_branch: ${{ env.UPSTREAM_BRANCH }}
           upstream_sync_branch: ${{ env.UPSTREAM_BRANCH }}
           upstream_sync_repo: ${{ env.UPSTREAM_REPO }}
           upstream_sync_repo: ${{ env.UPSTREAM_REPO }}
-      
+
       # Display a sample message based on the sync output var 'has_new_commits'
       # Display a sample message based on the sync output var 'has_new_commits'
       - name: New commits found
       - name: New commits found
         if: |
         if: |
           needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' &&
           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."
         run: echo "New commits were found to sync."
-    
+
       - name: No new commits
       - name: No new commits
         if: |
         if: |
           needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' && 
           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."
         run: echo "There were no new commits."
-      
+
       - name: Show value of 'has_new_commits'
       - name: Show value of 'has_new_commits'
         if: |
         if: |
           needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true'
           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: |
         run: |
           echo ${{ steps.sync.outputs.has_new_commits }}
           echo ${{ steps.sync.outputs.has_new_commits }}
           echo "NEW_COMMITS=${{ steps.sync.outputs.has_new_commits }}" >> $GITHUB_OUTPUT
           echo "NEW_COMMITS=${{ steps.sync.outputs.has_new_commits }}" >> $GITHUB_OUTPUT
@@ -231,12 +252,23 @@ jobs:
 
 
       # Patch Fastlane Match to not print tables
       # Patch Fastlane Match to not print tables
       - name: Patch Match 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
       # Install project dependencies
-      - name: Install project dependencies
+      - name: Install Project Dependencies
         run: bundle install
         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
       # Build signed Trio IPA file
       - name: Fastlane Build & Archive
       - name: Fastlane Build & Archive
         run: bundle exec fastlane build_trio
         run: bundle exec fastlane build_trio
@@ -247,7 +279,7 @@ jobs:
           FASTLANE_ISSUER_ID: ${{ secrets.FASTLANE_ISSUER_ID }}
           FASTLANE_ISSUER_ID: ${{ secrets.FASTLANE_ISSUER_ID }}
           FASTLANE_KEY: ${{ secrets.FASTLANE_KEY }}
           FASTLANE_KEY: ${{ secrets.FASTLANE_KEY }}
           MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
           MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
-      
+
       # Upload to TestFlight
       # Upload to TestFlight
       - name: Fastlane upload to TestFlight
       - name: Fastlane upload to TestFlight
         run: bundle exec fastlane release
         run: bundle exec fastlane release

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

@@ -12,11 +12,11 @@ jobs:
   certificates:
   certificates:
     name: Create Certificates
     name: Create Certificates
     needs: validate
     needs: validate
-    runs-on: macos-13
+    runs-on: macos-14
     steps:
     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
       # Checks-out the repo
       - name: Checkout Repo
       - name: Checkout Repo
@@ -24,12 +24,23 @@ jobs:
 
 
       # Patch Fastlane Match to not print tables
       # Patch Fastlane Match to not print tables
       - name: Patch Match 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
       # Install project dependencies
       - name: Install Project Dependencies
       - name: Install Project Dependencies
         run: bundle install
         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
       # Create or update certificates for app
       - name: Create Certificates
       - name: Create Certificates
         run: bundle exec fastlane certs
         run: bundle exec fastlane certs

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

@@ -5,7 +5,7 @@ on: [workflow_call, workflow_dispatch]
 jobs:
 jobs:
   validate-access-token:
   validate-access-token:
     name: Access
     name: Access
-    runs-on: macos-13
+    runs-on: macos-14
     env:
     env:
       GH_PAT: ${{ secrets.GH_PAT }}
       GH_PAT: ${{ secrets.GH_PAT }}
       GH_TOKEN: ${{ secrets.GH_PAT }}
       GH_TOKEN: ${{ secrets.GH_PAT }}
@@ -74,7 +74,7 @@ jobs:
   validate-match-secrets:
   validate-match-secrets:
     name: Match-Secrets
     name: Match-Secrets
     needs: validate-access-token
     needs: validate-access-token
-    runs-on: macos-13
+    runs-on: macos-14
     env:
     env:
       GH_TOKEN: ${{ secrets.GH_PAT }}
       GH_TOKEN: ${{ secrets.GH_PAT }}
     steps:
     steps:
@@ -112,7 +112,7 @@ jobs:
   validate-fastlane-secrets:
   validate-fastlane-secrets:
     name: Fastlane
     name: Fastlane
     needs: [validate-access-token, validate-match-secrets]
     needs: [validate-access-token, validate-match-secrets]
-    runs-on: macos-13
+    runs-on: macos-14
     env:
     env:
       GH_PAT: ${{ secrets.GH_PAT }}
       GH_PAT: ${{ secrets.GH_PAT }}
       GH_TOKEN: ${{ secrets.GH_PAT }}
       GH_TOKEN: ${{ secrets.GH_PAT }}
@@ -125,10 +125,13 @@ jobs:
       - name: Checkout Repo
       - name: Checkout Repo
         uses: actions/checkout@v4
         uses: actions/checkout@v4
 
 
-      # Install project dependencies
       - name: Install Project Dependencies
       - name: Install Project Dependencies
         run: bundle install
         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
       - name: Validate Fastlane Secrets
         run: |
         run: |
           # Validate Fastlane Secrets
           # 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."
             [ -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
           elif [ ${#FASTLANE_KEY_ID} -ne 10 ]; then
             failed=true
             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
           elif ! [[ $FASTLANE_KEY_ID =~ $FASTLANE_KEY_ID_PATTERN ]]; then
             failed=true
             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
           elif ! [[ $FASTLANE_ISSUER_ID =~ $FASTLANE_ISSUER_ID_PATTERN ]]; then
             failed=true
             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
           elif ! echo "$FASTLANE_KEY" | openssl pkcs8 -nocrypt >/dev/null; then
             failed=true
             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."
             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

@@ -297,6 +297,7 @@
 		B958F1B72BA0711600484851 /* MKRingProgressView in Frameworks */ = {isa = PBXBuildFile; productRef = B958F1B62BA0711600484851 /* MKRingProgressView */; };
 		B958F1B72BA0711600484851 /* MKRingProgressView in Frameworks */ = {isa = PBXBuildFile; productRef = B958F1B62BA0711600484851 /* MKRingProgressView */; };
 		B9CAAEFC2AE70836000F68BC /* branch.txt in Resources */ = {isa = PBXBuildFile; fileRef = B9CAAEFB2AE70836000F68BC /* branch.txt */; };
 		B9CAAEFC2AE70836000F68BC /* branch.txt in Resources */ = {isa = PBXBuildFile; fileRef = B9CAAEFB2AE70836000F68BC /* branch.txt */; };
 		BA00D96F7B2FF169A06FB530 /* CGMStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C018D1680307A31C9ED7120 /* CGMStateModel.swift */; };
 		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 */; };
 		BD1661312B82ADAB00256551 /* CustomProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1661302B82ADAB00256551 /* CustomProgressView.swift */; };
 		BD2B464E0745FBE7B79913F4 /* NightscoutConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF768BD6264FF7D71D66767 /* NightscoutConfigProvider.swift */; };
 		BD2B464E0745FBE7B79913F4 /* NightscoutConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF768BD6264FF7D71D66767 /* NightscoutConfigProvider.swift */; };
 		BD2FF1A02AE29D43005D1C5D /* CheckboxToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */; };
 		BD2FF1A02AE29D43005D1C5D /* CheckboxToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */; };
@@ -307,6 +308,8 @@
 		BD7DA9A92AE06E9200601B20 /* BolusCalculatorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9A82AE06E9200601B20 /* BolusCalculatorStateModel.swift */; };
 		BD7DA9A92AE06E9200601B20 /* BolusCalculatorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9A82AE06E9200601B20 /* BolusCalculatorStateModel.swift */; };
 		BD7DA9AC2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9AB2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift */; };
 		BD7DA9AC2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9AB2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift */; };
 		BDB3C1192C03DD1000CEEAA1 /* UserDefaultsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB3C1182C03DD1000CEEAA1 /* UserDefaultsExtension.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 */; };
 		BDBAACFA2C2D439700370AAE /* OverrideData.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBAACF92C2D439700370AAE /* OverrideData.swift */; };
 		BDC2EA452C3043B000E5BBD0 /* OverrideStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC2EA442C3043B000E5BBD0 /* OverrideStorage.swift */; };
 		BDC2EA452C3043B000E5BBD0 /* OverrideStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC2EA442C3043B000E5BBD0 /* OverrideStorage.swift */; };
 		BDC2EA472C3045AD00E5BBD0 /* Override.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC2EA462C3045AD00E5BBD0 /* Override.swift */; };
 		BDC2EA472C3045AD00E5BBD0 /* Override.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC2EA462C3045AD00E5BBD0 /* Override.swift */; };
@@ -320,6 +323,7 @@
 		BDF530D82B40F8AC002CAF43 /* LockScreenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDF530D72B40F8AC002CAF43 /* LockScreenView.swift */; };
 		BDF530D82B40F8AC002CAF43 /* LockScreenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDF530D72B40F8AC002CAF43 /* LockScreenView.swift */; };
 		BDFD165A2AE40438007F0DDA /* BolusRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFD16592AE40438007F0DDA /* BolusRootView.swift */; };
 		BDFD165A2AE40438007F0DDA /* BolusRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFD16592AE40438007F0DDA /* BolusRootView.swift */; };
 		BF1667ADE69E4B5B111CECAE /* ManualTempBasalProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680C4420C9A345D46D90D06C /* ManualTempBasalProvider.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 */; };
 		C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFCFE0781F9074C2917890E8 /* ManualTempBasalStateModel.swift */; };
 		CA370FC152BC98B3D1832968 /* BasalProfileEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */; };
 		CA370FC152BC98B3D1832968 /* BasalProfileEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */; };
 		CC41E29A2B1E1F460070974F /* HistoryLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC41E2992B1E1F460070974F /* HistoryLayout.swift */; };
 		CC41E29A2B1E1F460070974F /* HistoryLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC41E2992B1E1F460070974F /* HistoryLayout.swift */; };
@@ -431,6 +435,7 @@
 		DDD1631A2C4C695E00CD525A /* EditOverrideForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163192C4C695E00CD525A /* EditOverrideForm.swift */; };
 		DDD1631A2C4C695E00CD525A /* EditOverrideForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163192C4C695E00CD525A /* EditOverrideForm.swift */; };
 		DDD1631C2C4C697400CD525A /* AddOverrideForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD1631B2C4C697400CD525A /* AddOverrideForm.swift */; };
 		DDD1631C2C4C697400CD525A /* AddOverrideForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD1631B2C4C697400CD525A /* AddOverrideForm.swift */; };
 		DDD1631F2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DDD1631D2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodeld */; };
 		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 */; };
 		E00EEC0327368630002FF094 /* ServiceAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = E00EEBFD27368630002FF094 /* ServiceAssembly.swift */; };
 		E00EEC0427368630002FF094 /* SecurityAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = E00EEBFE27368630002FF094 /* SecurityAssembly.swift */; };
 		E00EEC0427368630002FF094 /* SecurityAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = E00EEBFE27368630002FF094 /* SecurityAssembly.swift */; };
 		E00EEC0527368630002FF094 /* StorageAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = E00EEBFF27368630002FF094 /* StorageAssembly.swift */; };
 		E00EEC0527368630002FF094 /* StorageAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = E00EEBFF27368630002FF094 /* StorageAssembly.swift */; };
@@ -897,6 +902,7 @@
 		B9B5C0607505A38F256BF99A /* CGMDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CGMDataFlow.swift; sourceTree = "<group>"; };
 		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; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxToggleStyle.swift; sourceTree = "<group>"; };
@@ -907,6 +913,8 @@
 		BD7DA9A82AE06E9200601B20 /* BolusCalculatorStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorStateModel.swift; sourceTree = "<group>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		BDC2EA462C3045AD00E5BBD0 /* Override.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Override.swift; sourceTree = "<group>"; };
@@ -921,6 +929,7 @@
 		BDFD16592AE40438007F0DDA /* BolusRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusRootView.swift; sourceTree = "<group>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		CC41E2992B1E1F460070974F /* HistoryLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryLayout.swift; sourceTree = "<group>"; };
@@ -1033,6 +1042,7 @@
 		DDD163192C4C695E00CD525A /* EditOverrideForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditOverrideForm.swift; sourceTree = "<group>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		E00EEBFF27368630002FF094 /* StorageAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StorageAssembly.swift; sourceTree = "<group>"; };
@@ -1834,6 +1844,7 @@
 				BD1661302B82ADAB00256551 /* CustomProgressView.swift */,
 				BD1661302B82ADAB00256551 /* CustomProgressView.swift */,
 				581516A32BCED84A00BF67D7 /* DebuggingIdentifiers.swift */,
 				581516A32BCED84A00BF67D7 /* DebuggingIdentifiers.swift */,
 				DD1DB7CB2BECCA1F0048B367 /* BuildDetails.swift */,
 				DD1DB7CB2BECCA1F0048B367 /* BuildDetails.swift */,
+				C20BC6CD2C66FBFD002BC1C6 /* Rounding.swift */,
 			);
 			);
 			path = Helpers;
 			path = Helpers;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -2109,6 +2120,7 @@
 				582FAE422C05102C00D1C13F /* CoreDataError.swift */,
 				582FAE422C05102C00D1C13F /* CoreDataError.swift */,
 				BDF34EBD2C0A31D000D51995 /* CustomNotification.swift */,
 				BDF34EBD2C0A31D000D51995 /* CustomNotification.swift */,
 				BDCD47AE2C1F3F1700F8BCD5 /* OverrideStored+helper.swift */,
 				BDCD47AE2C1F3F1700F8BCD5 /* OverrideStored+helper.swift */,
+				BDB899892C565D0B006F3298 /* CarbsGlucose+helper.swift */,
 			);
 			);
 			path = Helper;
 			path = Helper;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -2231,6 +2243,9 @@
 			children = (
 			children = (
 				BDFD16592AE40438007F0DDA /* BolusRootView.swift */,
 				BDFD16592AE40438007F0DDA /* BolusRootView.swift */,
 				58237D9D2BCF0A6B00A47A79 /* PopupView.swift */,
 				58237D9D2BCF0A6B00A47A79 /* PopupView.swift */,
+				BDB899872C564509006F3298 /* ForeCastChart.swift */,
+				BD0B2EF22C5998E600B3298F /* MealPresetView.swift */,
+				DDF847E52C5D66490049BB3B /* AddMealPresetView.swift */,
 			);
 			);
 			path = View;
 			path = View;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -3008,6 +3023,7 @@
 				DD57C4BD2C4C7103001A5B28 /* PumpEventStored+CoreDataProperties.swift in Sources */,
 				DD57C4BD2C4C7103001A5B28 /* PumpEventStored+CoreDataProperties.swift in Sources */,
 				DD57C4BE2C4C7103001A5B28 /* TempBasalStored+CoreDataClass.swift in Sources */,
 				DD57C4BE2C4C7103001A5B28 /* TempBasalStored+CoreDataClass.swift in Sources */,
 				DD57C4BF2C4C7103001A5B28 /* TempBasalStored+CoreDataProperties.swift in Sources */,
 				DD57C4BF2C4C7103001A5B28 /* TempBasalStored+CoreDataProperties.swift in Sources */,
+				BD0B2EF32C5998E600B3298F /* MealPresetView.swift in Sources */,
 				DD57C4C02C4C7103001A5B28 /* TempTargetsSlider+CoreDataClass.swift in Sources */,
 				DD57C4C02C4C7103001A5B28 /* TempTargetsSlider+CoreDataClass.swift in Sources */,
 				DD57C4C12C4C7103001A5B28 /* TempTargetsSlider+CoreDataProperties.swift in Sources */,
 				DD57C4C12C4C7103001A5B28 /* TempTargetsSlider+CoreDataProperties.swift in Sources */,
 				DD57C4C22C4C7103001A5B28 /* Forecast+CoreDataClass.swift in Sources */,
 				DD57C4C22C4C7103001A5B28 /* Forecast+CoreDataClass.swift in Sources */,
@@ -3028,6 +3044,7 @@
 				DD57C4D12C4C7103001A5B28 /* ImportError+CoreDataProperties.swift in Sources */,
 				DD57C4D12C4C7103001A5B28 /* ImportError+CoreDataProperties.swift in Sources */,
 				DD57C4D22C4C7103001A5B28 /* StatsData+CoreDataClass.swift in Sources */,
 				DD57C4D22C4C7103001A5B28 /* StatsData+CoreDataClass.swift in Sources */,
 				DD57C4D32C4C7103001A5B28 /* StatsData+CoreDataProperties.swift in Sources */,
 				DD57C4D32C4C7103001A5B28 /* StatsData+CoreDataProperties.swift in Sources */,
+				DDF847E62C5D66490049BB3B /* AddMealPresetView.swift in Sources */,
 				3811DEAE25C9D88300A708ED /* Cache.swift in Sources */,
 				3811DEAE25C9D88300A708ED /* Cache.swift in Sources */,
 				383420D625FFE38C002D46C1 /* LoopView.swift in Sources */,
 				383420D625FFE38C002D46C1 /* LoopView.swift in Sources */,
 				3811DEAD25C9D88300A708ED /* UserDefaults+Cache.swift in Sources */,
 				3811DEAD25C9D88300A708ED /* UserDefaults+Cache.swift in Sources */,
@@ -3077,9 +3094,11 @@
 				6B1A8D2E2B156EEF00E76752 /* LiveActivityBridge.swift in Sources */,
 				6B1A8D2E2B156EEF00E76752 /* LiveActivityBridge.swift in Sources */,
 				581516A92BCEEDF800BF67D7 /* NSPredicates.swift in Sources */,
 				581516A92BCEEDF800BF67D7 /* NSPredicates.swift in Sources */,
 				38DAB28A260D349500F74C1A /* FetchGlucoseManager.swift in Sources */,
 				38DAB28A260D349500F74C1A /* FetchGlucoseManager.swift in Sources */,
+				BDB8998A2C565D0C006F3298 /* CarbsGlucose+helper.swift in Sources */,
 				38F37828261260DC009DB701 /* Color+Extensions.swift in Sources */,
 				38F37828261260DC009DB701 /* Color+Extensions.swift in Sources */,
 				BDCD47AF2C1F3F1700F8BCD5 /* OverrideStored+helper.swift in Sources */,
 				BDCD47AF2C1F3F1700F8BCD5 /* OverrideStored+helper.swift in Sources */,
 				3811DE3F25C9D4A100A708ED /* SettingsStateModel.swift in Sources */,
 				3811DE3F25C9D4A100A708ED /* SettingsStateModel.swift in Sources */,
+				C20BC6CE2C66FBFD002BC1C6 /* Rounding.swift in Sources */,
 				CE7CA3582A064E2F004BE681 /* ListStateView.swift in Sources */,
 				CE7CA3582A064E2F004BE681 /* ListStateView.swift in Sources */,
 				193F6CDD2A512C8F001240FD /* Loops.swift in Sources */,
 				193F6CDD2A512C8F001240FD /* Loops.swift in Sources */,
 				38B4F3CB25E502E200E76A18 /* WeakObjectSet.swift in Sources */,
 				38B4F3CB25E502E200E76A18 /* WeakObjectSet.swift in Sources */,
@@ -3107,6 +3126,7 @@
 				CE7CA3532A064973004BE681 /* tempPresetIntent.swift in Sources */,
 				CE7CA3532A064973004BE681 /* tempPresetIntent.swift in Sources */,
 				D6DEC113821A7F1056C4AA1E /* NightscoutConfigDataFlow.swift in Sources */,
 				D6DEC113821A7F1056C4AA1E /* NightscoutConfigDataFlow.swift in Sources */,
 				38E98A3025F52FF700C0CED0 /* Config.swift in Sources */,
 				38E98A3025F52FF700C0CED0 /* Config.swift in Sources */,
+				BDB899882C564509006F3298 /* ForeCastChart.swift in Sources */,
 				110AEDE32C5193D200615CC9 /* BolusIntent.swift in Sources */,
 				110AEDE32C5193D200615CC9 /* BolusIntent.swift in Sources */,
 				DDD1631A2C4C695E00CD525A /* EditOverrideForm.swift in Sources */,
 				DDD1631A2C4C695E00CD525A /* EditOverrideForm.swift in Sources */,
 				CE1856F72ADC4869007E39C7 /* CarbPresetIntentRequest.swift in Sources */,
 				CE1856F72ADC4869007E39C7 /* CarbPresetIntentRequest.swift in Sources */,

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 1
FreeAPS/Resources/javascript/bundle/autosens.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 1
FreeAPS/Resources/javascript/bundle/autotune-prep.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 1
FreeAPS/Resources/javascript/bundle/iob.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 1
FreeAPS/Resources/javascript/bundle/meal.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 1
FreeAPS/Resources/javascript/bundle/profile.js


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

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

+ 31 - 8
FreeAPS/Sources/APS/APSManager.swift

@@ -24,6 +24,7 @@ protocol APSManager {
     func makeProfiles() async throws -> Bool
     func makeProfiles() async throws -> Bool
     func determineBasal() async -> Bool
     func determineBasal() async -> Bool
     func determineBasalSync() async
     func determineBasalSync() async
+    func simulateDetermineBasal(carbs: Decimal, iob: Decimal) async -> Determination?
     func roundBolus(amount: Decimal) -> Decimal
     func roundBolus(amount: Decimal) -> Decimal
     var lastError: CurrentValueSubject<Error?, Never> { get }
     var lastError: CurrentValueSubject<Error?, Never> { get }
     func cancelBolus() async
     func cancelBolus() async
@@ -323,9 +324,9 @@ final class BaseAPSManager: APSManager, Injectable {
         return nil
         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 {
         else {
             let result = try await openAPS.autosense()
             let result = try await openAPS.autosense()
             return result != nil
             return result != nil
@@ -372,11 +373,17 @@ final class BaseAPSManager: APSManager, Injectable {
 
 
         do {
         do {
             let now = Date()
             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 {
             if let determination = determination {
                 DispatchQueue.main.async {
                 DispatchQueue.main.async {
@@ -398,6 +405,18 @@ final class BaseAPSManager: APSManager, Injectable {
         _ = await determineBasal()
         _ = 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 {
     func makeProfiles() async throws -> Bool {
         let tunedProfile = await openAPS.makeProfiles(useAutotune: settings.useAutotune)
         let tunedProfile = await openAPS.makeProfiles(useAutotune: settings.useAutotune)
         if let basalProfile = tunedProfile?.basalProfile {
         if let basalProfile = tunedProfile?.basalProfile {
@@ -420,6 +439,10 @@ final class BaseAPSManager: APSManager, Injectable {
     private var bolusReporter: DoseProgressReporter?
     private var bolusReporter: DoseProgressReporter?
 
 
     func enactBolus(amount: Double, isSMB: Bool) async {
     func enactBolus(amount: Double, isSMB: Bool) async {
+        if amount <= 0 {
+            return
+        }
+
         if let error = verifyStatus() {
         if let error = verifyStatus() {
             processError(error)
             processError(error)
             processQueue.async {
             processQueue.async {

+ 103 - 37
FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift

@@ -150,7 +150,7 @@ final class OpenAPS {
         }
         }
     }
     }
 
 
-    private func fetchAndProcessCarbs() async -> String {
+    private func fetchAndProcessCarbs(additionalCarbs: Decimal? = nil) async -> String {
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
             ofType: CarbEntryStored.self,
             onContext: context,
             onContext: context,
@@ -163,10 +163,40 @@ final class OpenAPS {
             return ""
             return ""
         }
         }
 
 
-        // convert to JSON
-        return await context.perform {
-            return self.jsonConverter.convertToJSON(carbResults)
+        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]? {
     private func fetchPumpHistoryObjectIDs() async -> [NSManagedObjectID]? {
@@ -188,32 +218,19 @@ final class OpenAPS {
         }
         }
     }
     }
 
 
-    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
         // Return an empty JSON object if the list of object IDs is empty
         guard !pumpHistoryObjectIDs.isEmpty else { return "{}" }
         guard !pumpHistoryObjectIDs.isEmpty else { return "{}" }
 
 
         // Execute all operations on the background context
         // Execute all operations on the background context
         return await context.perform {
         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)
-                }
+            // Load and map pump events to DTOs
+            var dtos = self.loadAndMapPumpEvents(pumpHistoryObjectIDs)
 
 
-                if let tempBasalDurationDTO = event.toTempBasalDurationDTOEnum() {
-                    eventDTOs.append(tempBasalDurationDTO)
-                }
-
-                if let tempBasalDTO = event.toTempBasalDTOEnum() {
-                    eventDTOs.append(tempBasalDTO)
-                }
-
-                return eventDTOs
+            // 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
             // Convert the DTOs to JSON
@@ -221,18 +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
+    }
 
 
-        // clock
-        self.storage.save(clock, as: Monitor.clock)
+    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)
+    }
+
+    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
         // temp_basal
         let tempBasal = currentTemp.rawJSON
         let tempBasal = currentTemp.rawJSON
 
 
         // Perform asynchronous calls in parallel
         // Perform asynchronous calls in parallel
         async let pumpHistoryObjectIDs = fetchPumpHistoryObjectIDs() ?? []
         async let pumpHistoryObjectIDs = fetchPumpHistoryObjectIDs() ?? []
-        async let carbs = fetchAndProcessCarbs()
+        async let carbs = fetchAndProcessCarbs(additionalCarbs: carbs ?? 0)
         async let glucose = fetchAndProcessGlucose()
         async let glucose = fetchAndProcessGlucose()
         async let oref2 = oref2()
         async let oref2 = oref2()
         async let profileAsync = loadFileFromStorageAsync(name: Settings.profile)
         async let profileAsync = loadFileFromStorageAsync(name: Settings.profile)
@@ -253,7 +316,7 @@ final class OpenAPS {
             reservoir,
             reservoir,
             preferences
             preferences
         ) = await (
         ) = await (
-            parsePumpHistory(await pumpHistoryObjectIDs),
+            parsePumpHistory(await pumpHistoryObjectIDs, iob: iob),
             carbs,
             carbs,
             glucose,
             glucose,
             oref2,
             oref2,
@@ -264,8 +327,7 @@ final class OpenAPS {
             preferencesAsync
             preferencesAsync
         )
         )
 
 
-        // TODO: - Save and fetch profile/basalProfile in/from UserDefaults!
-        // Meal
+        // Meal calculation
         let meal = try await self.meal(
         let meal = try await self.meal(
             pumphistory: pumpHistoryJSON,
             pumphistory: pumpHistoryJSON,
             profile: profile,
             profile: profile,
@@ -275,7 +337,7 @@ final class OpenAPS {
             glucose: glucoseAsJSON
             glucose: glucoseAsJSON
         )
         )
 
 
-        // IOB
+        // IOB calculation
         let iob = try await self.iob(
         let iob = try await self.iob(
             pumphistory: pumpHistoryJSON,
             pumphistory: pumpHistoryJSON,
             profile: profile,
             profile: profile,
@@ -284,7 +346,9 @@ final class OpenAPS {
         )
         )
 
 
         // TODO: refactor this to core data
         // TODO: refactor this to core data
-        storage.save(iob, as: Monitor.iob)
+        if !simulation {
+            storage.save(iob, as: Monitor.iob)
+        }
 
 
         // Determine basal
         // Determine basal
         let orefDetermination = try await determineBasal(
         let orefDetermination = try await determineBasal(
@@ -309,8 +373,10 @@ final class OpenAPS {
             // AAPS does it the same way! we'll follow their example!
             // AAPS does it the same way! we'll follow their example!
             determination.timestamp = deliverAt
             determination.timestamp = deliverAt
 
 
-            // save to core data asynchronously
-            await processDetermination(determination)
+            if !simulation {
+                // save to core data asynchronously
+                await processDetermination(determination)
+            }
 
 
             return determination
             return determination
         } else {
         } else {
@@ -546,7 +612,7 @@ final class OpenAPS {
         debug(.openAPS, "AUTOSENS: \(autosenseResult)")
         debug(.openAPS, "AUTOSENS: \(autosenseResult)")
         if var autosens = Autosens(from: autosenseResult) {
         if var autosens = Autosens(from: autosenseResult) {
             autosens.timestamp = Date()
             autosens.timestamp = Date()
-            storage.save(autosens, as: Settings.autosense)
+            await storage.saveAsync(autosens, as: Settings.autosense)
 
 
             return autosens
             return autosens
         } else {
         } else {

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

@@ -156,6 +156,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             newItem.carbs = Double(truncating: NSDecimalNumber(decimal: entry.carbs))
             newItem.carbs = Double(truncating: NSDecimalNumber(decimal: entry.carbs))
             newItem.fat = Double(truncating: NSDecimalNumber(decimal: entry.fat ?? 0))
             newItem.fat = Double(truncating: NSDecimalNumber(decimal: entry.fat ?? 0))
             newItem.protein = Double(truncating: NSDecimalNumber(decimal: entry.protein ?? 0))
             newItem.protein = Double(truncating: NSDecimalNumber(decimal: entry.protein ?? 0))
+            newItem.note = entry.note
             newItem.id = UUID()
             newItem.id = UUID()
             newItem.isFPU = false
             newItem.isFPU = false
             newItem.isUploadedToNS = false
             newItem.isUploadedToNS = false
@@ -264,6 +265,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
                     enteredBy: CarbsEntry.manual,
                     enteredBy: CarbsEntry.manual,
                     bolus: nil,
                     bolus: nil,
                     insulin: nil,
                     insulin: nil,
+                    notes: result.note,
                     carbs: Decimal(result.carbs),
                     carbs: Decimal(result.carbs),
                     fat: Decimal(result.fat),
                     fat: Decimal(result.fat),
                     protein: Decimal(result.protein),
                     protein: Decimal(result.protein),

+ 19 - 10
FreeAPS/Sources/APS/Storage/DeterminationStorage.swift

@@ -78,25 +78,32 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
     }
     }
 
 
     // Convert NSSet to array of Ints for Predictions
     // 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)
         let forecastIDs = await getForecastIDs(for: determinationID, in: backgroundContext)
 
 
         var forecastValuesList: [Int] = []
         var forecastValuesList: [Int] = []
 
 
         for forecastID in forecastIDs {
         for forecastID in forecastIDs {
-            let forecastValueIDs = await getForecastValueIDs(for: forecastID, in: backgroundContext)
-
             await backgroundContext.perform {
             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? {
     func getOrefDeterminationNotYetUploadedToNightscout(_ determinationIds: [NSManagedObjectID]) async -> Determination? {
@@ -155,8 +162,10 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
                     )
                     )
                 }
                 }
             } catch {
             } catch {
-                debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch managed object with error: \(error.localizedDescription)")
-             }
+                debugPrint(
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch managed object with error: \(error.localizedDescription)"
+                )
+            }
 
 
             return result
             return result
         }
         }

+ 6 - 0
FreeAPS/Sources/APS/Storage/GlucoseStorage.swift

@@ -7,6 +7,7 @@ import Swinject
 
 
 protocol GlucoseStorage {
 protocol GlucoseStorage {
     func storeGlucose(_ glucose: [BloodGlucose])
     func storeGlucose(_ glucose: [BloodGlucose])
+    func isGlucoseDataFresh(_ glucoseDate: Date?) -> Bool
     func syncDate() -> Date
     func syncDate() -> Date
     func filterTooFrequentGlucose(_ glucose: [BloodGlucose], at: Date) -> [BloodGlucose]
     func filterTooFrequentGlucose(_ glucose: [BloodGlucose], at: Date) -> [BloodGlucose]
     func lastGlucoseDate() -> Date
     func lastGlucoseDate() -> Date
@@ -158,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 {
     func syncDate() -> Date {
         let fr = GlucoseStored.fetchRequest()
         let fr = GlucoseStored.fetchRequest()
         fr.predicate = NSPredicate.predicateForOneDayAgo
         fr.predicate = NSPredicate.predicateForOneDayAgo

+ 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 {
 extension Int {
     var asMmolL: Decimal {
     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 {
 extension Decimal {
     var asMmolL: Decimal {
     var asMmolL: Decimal {
-        self * GlucoseUnits.exchangeRate
+        FreeAPS.rounded(self * GlucoseUnits.exchangeRate, scale: 1, roundingMode: .plain)
     }
     }
 
 
     var asMgdL: Decimal {
     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 {
 extension Double {
     var asMmolL: Decimal {
     var asMmolL: Decimal {
-        Decimal(self) * GlucoseUnits.exchangeRate
+        FreeAPS.rounded(Decimal(self) * GlucoseUnits.exchangeRate, scale: 1, roundingMode: .plain)
     }
     }
 
 
     var asMgdL: Decimal {
     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 {
 extension BloodGlucose: SavitzkyGolaySmoothable {

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

@@ -59,6 +59,7 @@ struct FreeAPSSettings: JSON, Equatable {
     var yGridLines: Bool = true
     var yGridLines: Bool = true
     var oneDimensionalGraph: Bool = false
     var oneDimensionalGraph: Bool = false
     var rulerMarks: Bool = true
     var rulerMarks: Bool = true
+    var displayForecastsAsLines: Bool = false
     var maxCarbs: Decimal = 250
     var maxCarbs: Decimal = 250
     var maxFat: Decimal = 250
     var maxFat: Decimal = 250
     var maxProtein: Decimal = 250
     var maxProtein: Decimal = 250
@@ -281,6 +282,10 @@ extension FreeAPSSettings: Decodable {
             settings.rulerMarks = rulerMarks
             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) {
         if let overrideHbA1cUnit = try? container.decode(Bool.self, forKey: .overrideHbA1cUnit) {
             settings.overrideHbA1cUnit = overrideHbA1cUnit
             settings.overrideHbA1cUnit = overrideHbA1cUnit
         }
         }

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

@@ -118,14 +118,14 @@ extension AutotuneConfig {
                             .foregroundColor(.red)
                             .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)
             .scrollContentBackground(.hidden).background(color)

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

@@ -3,5 +3,9 @@ enum Bolus {
 }
 }
 
 
 protocol BolusProvider: Provider {
 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 {
 extension Bolus {
     final class Provider: BaseProvider, BolusProvider {
     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(from: OpenAPS.defaults(for: OpenAPS.Settings.settings))
                 ?? PumpSettings(insulinActionCurve: 6, maxBolus: 10, maxBasal: 2)
                 ?? 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))
                 ?? [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: []
+                )
+        }
     }
     }
 }
 }

+ 278 - 183
FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift

@@ -17,6 +17,9 @@ extension Bolus {
         @Injected() var glucoseStorage: GlucoseStorage!
         @Injected() var glucoseStorage: GlucoseStorage!
         @Injected() var determinationStorage: DeterminationStorage!
         @Injected() var determinationStorage: DeterminationStorage!
 
 
+        @Published var lowGlucose: Decimal = 70
+        @Published var highGlucose: Decimal = 180
+
         @Published var predictions: Predictions?
         @Published var predictions: Predictions?
         @Published var amount: Decimal = 0
         @Published var amount: Decimal = 0
         @Published var insulinRecommended: Decimal = 0
         @Published var insulinRecommended: Decimal = 0
@@ -66,6 +69,10 @@ extension Bolus {
         @Published var displayPresets: Bool = true
         @Published var displayPresets: Bool = true
 
 
         @Published var currentBasal: Decimal = 0
         @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 sweetMeals: Bool = false
         @Published var sweetMealFactor: Decimal = 0
         @Published var sweetMealFactor: Decimal = 0
         @Published var useSuperBolus: Bool = false
         @Published var useSuperBolus: Bool = false
@@ -96,10 +103,20 @@ extension Bolus {
         @Published var showInfo: Bool = false
         @Published var showInfo: Bool = false
         @Published var glucoseFromPersistence: [GlucoseStored] = []
         @Published var glucoseFromPersistence: [GlucoseStored] = []
         @Published var determination: [OrefDetermination] = []
         @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 now = Date.now
 
 
-        let context = CoreDataStack.shared.persistentContainer.viewContext
+        let viewContext = CoreDataStack.shared.persistentContainer.viewContext
         let backgroundContext = CoreDataStack.shared.newTaskContext()
         let backgroundContext = CoreDataStack.shared.newTaskContext()
 
 
         private var coreDataObserver: CoreDataObserver?
         private var coreDataObserver: CoreDataObserver?
@@ -110,16 +127,24 @@ extension Bolus {
             setupGlucoseNotification()
             setupGlucoseNotification()
             coreDataObserver = CoreDataObserver()
             coreDataObserver = CoreDataObserver()
             registerHandlers()
             registerHandlers()
-
             setupGlucoseArray()
             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(DeterminationObserver.self, observer: self)
             broadcaster.register(BolusFailureObserver.self, observer: self)
             broadcaster.register(BolusFailureObserver.self, observer: self)
             units = settingsManager.settings.units
             units = settingsManager.settings.units
             percentage = settingsManager.settings.insulinReqPercentage
             percentage = settingsManager.settings.insulinReqPercentage
-            maxBolus = provider.pumpSettings().maxBolus
-            // added
             fraction = settings.settings.overrideFactor
             fraction = settings.settings.overrideFactor
             useCalc = settings.settings.useCalc
             useCalc = settings.settings.useCalc
             fattyMeals = settings.settings.fattyMeals
             fattyMeals = settings.settings.fattyMeals
@@ -128,11 +153,17 @@ extension Bolus {
             sweetMealFactor = settings.settings.sweetMealFactor
             sweetMealFactor = settings.settings.sweetMealFactor
             displayPresets = settings.settings.displayPresets
             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
             maxCarbs = settings.settings.maxCarbs
             maxFat = settings.settings.maxFat
             maxFat = settings.settings.maxFat
             maxProtein = settings.settings.maxProtein
             maxProtein = settings.settings.maxProtein
             skipBolus = settingsManager.settings.skipBolusScreenAfterCarbs
             skipBolus = settingsManager.settings.skipBolusScreenAfterCarbs
             useFPUconversion = settingsManager.settings.useFPUconversion
             useFPUconversion = settingsManager.settings.useFPUconversion
+            smooth = settingsManager.settings.smoothGlucose
 
 
             if waitForSuggestionInitial {
             if waitForSuggestionInitial {
                 Task {
                 Task {
@@ -148,47 +179,103 @@ extension Bolus {
 
 
         // MARK: - Basal
         // 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 now = Date()
             let calendar = Calendar.current
             let calendar = Calendar.current
             let dateFormatter = DateFormatter()
             let dateFormatter = DateFormatter()
             dateFormatter.dateFormat = "HH:mm:ss"
             dateFormatter.dateFormat = "HH:mm:ss"
             dateFormatter.timeZone = TimeZone.current
             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 {
                 guard let entryTime = dateFormatter.date(from: entry.start) else {
                     print("Invalid entry start time: \(entry.start)")
                     print("Invalid entry start time: \(entry.start)")
                     continue
                     continue
                 }
                 }
 
 
-                // Combine the current date with the time from entry.start
+                let entryComponents = calendar.dateComponents([.hour, .minute, .second], from: entryTime)
                 let entryStartTime = calendar.date(
                 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
                     of: now
                 )!
                 )!
 
 
                 let entryEndTime: Date
                 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
                         of: now
                     )!
                     )!
-                    entryEndTime = nextEntryStartTime
                 } else {
                 } 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)!
                     entryEndTime = calendar.date(byAdding: .day, value: 1, to: entryStartTime)!
                 }
                 }
 
 
                 if now >= entryStartTime, now < entryEndTime {
                 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
                 }
                 }
             }
             }
         }
         }
@@ -197,11 +284,7 @@ extension Bolus {
 
 
         /// Calculate insulin recommendation
         /// Calculate insulin recommendation
         func calculateInsulin() -> Decimal {
         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
             // insulin needed for the current blood glucose
             targetDifference = currentBG - target
             targetDifference = currentBG - target
@@ -281,9 +364,10 @@ extension Bolus {
                 await saveMeal()
                 await saveMeal()
 
 
                 // if glucose data is stale end the custom loading animation by hiding the modal
                 // 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()
+                }
             }
             }
         }
         }
 
 
@@ -327,29 +411,6 @@ 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
         // MARK: - EXTERNAL INSULIN
 
 
         func addExternalInsulin() async {
         func addExternalInsulin() async {
@@ -389,11 +450,11 @@ extension Bolus {
             guard carbs > 0 || fat > 0 || protein > 0 else { return }
             guard carbs > 0 || fat > 0 || protein > 0 else { return }
 
 
             await MainActor.run {
             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
-               }
+                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(
             let carbsToStore = [CarbsEntry(
                 id: id_,
                 id: id_,
@@ -420,11 +481,11 @@ extension Bolus {
 
 
         func deletePreset() {
         func deletePreset() {
             if selection != nil {
             if selection != nil {
-                context.delete(selection!)
+                viewContext.delete(selection!)
 
 
                 do {
                 do {
-                    guard context.hasChanges else { return }
-                    try context.save()
+                    guard viewContext.hasChanges else { return }
+                    try viewContext.save()
                 } catch {
                 } catch {
                     print(error.localizedDescription)
                     print(error.localizedDescription)
                 }
                 }
@@ -456,79 +517,6 @@ extension Bolus {
         func addToSummation() {
         func addToSummation() {
             summation.append(selection?.dish ?? "")
             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
-        }
     }
     }
 }
 }
 
 
@@ -556,7 +544,10 @@ extension Bolus.StateModel {
     private func registerHandlers() {
     private func registerHandlers() {
         coreDataObserver?.registerHandler(for: "OrefDetermination") { [weak self] in
         coreDataObserver?.registerHandler(for: "OrefDetermination") { [weak self] in
             guard let self = self else { return }
             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
         // Due to the Batch insert this only is used for observing Deletion of Glucose entries
@@ -588,7 +579,8 @@ extension Bolus.StateModel {
     private func setupGlucoseArray() {
     private func setupGlucoseArray() {
         Task {
         Task {
             let ids = await self.fetchGlucose()
             let ids = await self.fetchGlucose()
-            await updateGlucoseArray(with: ids)
+            let glucoseObjects: [GlucoseStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
+            await updateGlucoseArray(with: glucoseObjects)
         }
         }
     }
     }
 
 
@@ -596,10 +588,10 @@ extension Bolus.StateModel {
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             ofType: GlucoseStored.self,
             onContext: backgroundContext,
             onContext: backgroundContext,
-            predicate: NSPredicate.predicateFor30MinAgo,
+            predicate: NSPredicate.glucose,
             key: "date",
             key: "date",
             ascending: false,
             ascending: false,
-            fetchLimit: 3
+            fetchLimit: 288
         )
         )
 
 
         guard let fetchedResults = results as? [GlucoseStored] else { return [] }
         guard let fetchedResults = results as? [GlucoseStored] else { return [] }
@@ -609,61 +601,164 @@ extension Bolus.StateModel {
         }
         }
     }
     }
 
 
-    @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
     // 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))
+    }
+}

+ 86 - 207
FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift

@@ -10,6 +10,7 @@ extension Bolus {
             case carbs
             case carbs
             case fat
             case fat
             case protein
             case protein
+            case bolus
         }
         }
 
 
         @FocusState private var focusedField: FocusedField?
         @FocusState private var focusedField: FocusedField?
@@ -18,17 +19,12 @@ extension Bolus {
 
 
         @StateObject var state = StateModel()
         @StateObject var state = StateModel()
 
 
-        @State private var showAlert = false
+        @State private var showPresetSheet = false
         @State private var autofocus: Bool = true
         @State private var autofocus: Bool = true
         @State private var calculatorDetent = PresentationDetent.medium
         @State private var calculatorDetent = PresentationDetent.medium
         @State private var pushed: Bool = false
         @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?
         @State private var debounce: DispatchWorkItem?
 
 
-        @Environment(\.managedObjectContext) var moc
-
         private enum Config {
         private enum Config {
             static let dividerHeight: CGFloat = 2
             static let dividerHeight: CGFloat = 2
             static let spacing: CGFloat = 3
             static let spacing: CGFloat = 3
@@ -36,11 +32,6 @@ extension Bolus {
 
 
         @Environment(\.colorScheme) var colorScheme
         @Environment(\.colorScheme) var colorScheme
 
 
-        @FetchRequest(
-            entity: MealPresetStored.entity(),
-            sortDescriptors: [NSSortDescriptor(key: "dish", ascending: true)]
-        ) var carbPresets: FetchedResults<MealPresetStored>
-
         private var formatter: NumberFormatter {
         private var formatter: NumberFormatter {
             let formatter = NumberFormatter()
             let formatter = NumberFormatter()
             formatter.numberStyle = .decimal
             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.
         /// Handles macro input (carb, fat, protein) in a debounced fashion.
         func handleDebouncedInput() {
         func handleDebouncedInput() {
             debounce?.cancel()
             debounce?.cancel()
             debounce = DispatchWorkItem { [self] in
             debounce = DispatchWorkItem { [self] in
                 state.insulinCalculated = state.calculateInsulin()
                 state.insulinCalculated = state.calculateInsulin()
+                Task {
+                    await state.updateForecasts()
+                }
             }
             }
             if let debounce = debounce {
             if let debounce = debounce {
                 DispatchQueue.main.asyncAfter(deadline: .now() + 0.35, execute: 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 {
         @ViewBuilder private func proteinAndFat() -> some View {
             HStack {
             HStack {
                 Text("Fat").foregroundColor(.orange)
                 Text("Fat").foregroundColor(.orange)
                 Spacer()
                 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)
                 Text("g").foregroundColor(.secondary)
             }
             }
             HStack {
             HStack {
@@ -269,8 +113,10 @@ extension Bolus {
                     text: $state.protein,
                     text: $state.protein,
                     placeholder: "0",
                     placeholder: "0",
                     keyboardType: .numberPad,
                     keyboardType: .numberPad,
-                    numberFormatter: mealFormatter
-                )
+                    numberFormatter: mealFormatter,
+                    previousTextField: { focusOnPreviousTextField(index: 3) },
+                    nextTextField: { focusOnNextTextField(index: 3) }
+                ).focused($focusedField, equals: .protein)
                 Text("g").foregroundColor(.secondary)
                 Text("g").foregroundColor(.secondary)
             }
             }
         }
         }
@@ -283,15 +129,43 @@ extension Bolus {
                     text: $state.carbs,
                     text: $state.carbs,
                     placeholder: "0",
                     placeholder: "0",
                     keyboardType: .numberPad,
                     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)
                 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 {
         var body: some View {
             ZStack(alignment: .center) {
             ZStack(alignment: .center) {
                 VStack {
                 VStack {
@@ -303,20 +177,6 @@ extension Bolus {
                                 proteinAndFat()
                                 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
                             // Time
                             HStack {
                             HStack {
                                 Text("Time").foregroundStyle(Color.secondary)
                                 Text("Time").foregroundStyle(Color.secondary)
@@ -341,18 +201,12 @@ extension Bolus {
                                     label: { Image(systemName: "plus.circle") }.tint(.blue).buttonStyle(.borderless)
                                     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)
                         }.listRowBackground(Color.chart)
 
 
-                        if state.displayPresets {
-                            Section {
-                                mealPresets
-                            }.listRowBackground(Color.chart)
-                        }
-
                         Section {
                         Section {
                             HStack {
                             HStack {
                                 Button(action: {
                                 Button(action: {
@@ -420,19 +274,29 @@ extension Bolus {
                                     placeholder: "0",
                                     placeholder: "0",
                                     textColor: colorScheme == .dark ? .white : .blue,
                                     textColor: colorScheme == .dark ? .white : .blue,
                                     maxLength: 5,
                                     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)
                                 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)
                         }.listRowBackground(Color.chart)
+
+                        Section {
+                            ForeCastChart(state: state, units: $state.units)
+                                .padding(.vertical)
+                        }.listRowBackground(Color.chart)
                     }
                     }
                 }
                 }
                 .safeAreaInset(edge: .bottom, spacing: 0) {
                 .safeAreaInset(edge: .bottom, spacing: 0) {
@@ -455,6 +319,16 @@ extension Bolus {
                         Text("Close")
                         Text("Close")
                     }
                     }
                 }
                 }
+                ToolbarItem(placement: .topBarTrailing) {
+                    Button(action: {
+                        showPresetSheet = true
+                    }, label: {
+                        HStack {
+                            Text("Presets")
+                            Image(systemName: "plus")
+                        }
+                    })
+                }
             })
             })
             .onAppear {
             .onAppear {
                 configureView {
                 configureView {
@@ -471,6 +345,11 @@ extension Bolus {
                         selection: $calculatorDetent
                         selection: $calculatorDetent
                     )
                     )
             }
             }
+            .sheet(isPresented: $showPresetSheet, onDismiss: {
+                showPresetSheet = false
+            }) {
+                MealPresetView(state: state)
+            }
         }
         }
 
 
         var progressText: ProgressText {
         var progressText: ProgressText {

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

+ 28 - 17
FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift

@@ -128,7 +128,9 @@ extension DataTable {
                         .background(color)
                         .background(color)
                 }.blur(radius: state.waitForSuggestion ? 8 : 0)
                 }.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)
                     CustomProgressView(text: progressText.rawValue)
                 }
                 }
             })
             })
@@ -450,24 +452,33 @@ extension DataTable {
         }
         }
 
 
         @ViewBuilder private func mealView(_ meal: CarbEntryStored) -> some View {
         @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 {
             .swipeActions {
                 Button(
                 Button(

+ 181 - 159
FreeAPS/Sources/Modules/Home/HomeStateModel.swift

@@ -12,6 +12,7 @@ extension Home {
         @Injected() var fetchGlucoseManager: FetchGlucoseManager!
         @Injected() var fetchGlucoseManager: FetchGlucoseManager!
         @Injected() var nightscoutManager: NightscoutManager!
         @Injected() var nightscoutManager: NightscoutManager!
         @Injected() var determinationStorage: DeterminationStorage!
         @Injected() var determinationStorage: DeterminationStorage!
+        @Injected() var glucoseStorage: GlucoseStorage!
         private let timer = DispatchTimer(timeInterval: 5)
         private let timer = DispatchTimer(timeInterval: 5)
         private(set) var filteredHours = 24
         private(set) var filteredHours = 24
         @Published var manualGlucose: [BloodGlucose] = []
         @Published var manualGlucose: [BloodGlucose] = []
@@ -46,8 +47,8 @@ extension Home {
         @Published var manualTempBasal = false
         @Published var manualTempBasal = false
         @Published var smooth = false
         @Published var smooth = false
         @Published var maxValue: Decimal = 1.2
         @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 overrideUnit: Bool = false
         @Published var displayXgridLines: Bool = false
         @Published var displayXgridLines: Bool = false
         @Published var displayYgridLines: Bool = false
         @Published var displayYgridLines: Bool = false
@@ -81,6 +82,11 @@ extension Home {
         @Published var pumpStatusHighlightMessage: String? = nil
         @Published var pumpStatusHighlightMessage: String? = nil
         @Published var cgmAvailable: Bool = false
         @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 context = CoreDataStack.shared.newTaskContext()
         let viewContext = CoreDataStack.shared.persistentContainer.viewContext
         let viewContext = CoreDataStack.shared.persistentContainer.viewContext
 
 
@@ -120,8 +126,8 @@ extension Home {
             setupCurrentTempTarget()
             setupCurrentTempTarget()
             smooth = settingsManager.settings.smoothGlucose
             smooth = settingsManager.settings.smoothGlucose
             maxValue = settingsManager.preferences.autosensMax
             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
             overrideUnit = settingsManager.settings.overrideHbA1cUnit
             displayXgridLines = settingsManager.settings.xGridLines
             displayXgridLines = settingsManager.settings.xGridLines
             displayYgridLines = settingsManager.settings.yGridLines
             displayYgridLines = settingsManager.settings.yGridLines
@@ -129,6 +135,8 @@ extension Home {
             tins = settingsManager.settings.tins
             tins = settingsManager.settings.tins
             cgmAvailable = fetchGlucoseManager.cgmGlucoseSourceType != CGMType.none
             cgmAvailable = fetchGlucoseManager.cgmGlucoseSourceType != CGMType.none
 
 
+            displayForecastsAsLines = settingsManager.settings.displayForecastsAsLines
+
             broadcaster.register(GlucoseObserver.self, observer: self)
             broadcaster.register(GlucoseObserver.self, observer: self)
             broadcaster.register(DeterminationObserver.self, observer: self)
             broadcaster.register(DeterminationObserver.self, observer: self)
             broadcaster.register(SettingsObserver.self, observer: self)
             broadcaster.register(SettingsObserver.self, observer: self)
@@ -229,10 +237,7 @@ extension Home {
         private func registerHandlers() {
         private func registerHandlers() {
             coreDataObserver?.registerHandler(for: "OrefDetermination") { [weak self] in
             coreDataObserver?.registerHandler(for: "OrefDetermination") { [weak self] in
                 guard let self = self else { return }
                 guard let self = self else { return }
-                Task {
-                    self.setupDeterminationsArray()
-                    await self.updateForecastData()
-                }
+                self.setupDeterminationsArray()
             }
             }
 
 
             coreDataObserver?.registerHandler(for: "GlucoseStored") { [weak self] in
             coreDataObserver?.registerHandler(for: "GlucoseStored") { [weak self] in
@@ -289,6 +294,11 @@ extension Home {
             provider.heartbeatNow()
             provider.heartbeatNow()
         }
         }
 
 
+        func showProgressView() {
+            glucoseStorage
+                .isGlucoseDataFresh(glucoseFromPersistence.first?.date) ? (waitForSuggestion = true) : (waitForSuggestion = false)
+        }
+
         func cancelBolus() {
         func cancelBolus() {
             Task {
             Task {
                 await apsManager.cancelBolus()
                 await apsManager.cancelBolus()
@@ -453,12 +463,13 @@ extension Home.StateModel:
         animatedBackground = settingsManager.settings.animatedBackground
         animatedBackground = settingsManager.settings.animatedBackground
         manualTempBasal = apsManager.isManualTempBasal
         manualTempBasal = apsManager.isManualTempBasal
         smooth = settingsManager.settings.smoothGlucose
         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
         overrideUnit = settingsManager.settings.overrideHbA1cUnit
         displayXgridLines = settingsManager.settings.xGridLines
         displayXgridLines = settingsManager.settings.xGridLines
         displayYgridLines = settingsManager.settings.yGridLines
         displayYgridLines = settingsManager.settings.yGridLines
         thresholdLines = settingsManager.settings.rulerMarks
         thresholdLines = settingsManager.settings.rulerMarks
+        displayForecastsAsLines = settingsManager.settings.displayForecastsAsLines
         tins = settingsManager.settings.tins
         tins = settingsManager.settings.tins
         cgmAvailable = (fetchGlucoseManager.cgmGlucoseSourceType != CGMType.none)
         cgmAvailable = (fetchGlucoseManager.cgmGlucoseSourceType != CGMType.none)
         displayPumpStatusHighlightMessage()
         displayPumpStatusHighlightMessage()
@@ -560,7 +571,8 @@ extension Home.StateModel {
     private func setupGlucoseArray() {
     private func setupGlucoseArray() {
         Task {
         Task {
             let ids = await self.fetchGlucose()
             let ids = await self.fetchGlucose()
-            await updateGlucoseArray(with: ids)
+            let glucoseObjects: [GlucoseStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
+            await updateGlucoseArray(with: glucoseObjects)
         }
         }
     }
     }
 
 
@@ -581,24 +593,17 @@ extension Home.StateModel {
         }
         }
     }
     }
 
 
-    @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
     // Setup Manual Glucose
     private func setupManualGlucoseArray() {
     private func setupManualGlucoseArray() {
         Task {
         Task {
             let ids = await self.fetchManualGlucose()
             let ids = await self.fetchManualGlucose()
-            await updateManualGlucoseArray(with: ids)
+            let manualGlucoseObjects: [GlucoseStored] = await CoreDataStack.shared
+                .getNSManagedObject(with: ids, context: viewContext)
+            await updateManualGlucoseArray(with: manualGlucoseObjects)
         }
         }
     }
     }
 
 
@@ -619,24 +624,16 @@ extension Home.StateModel {
         }
         }
     }
     }
 
 
-    @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
     // Setup Carbs
     private func setupCarbsArray() {
     private func setupCarbsArray() {
         Task {
         Task {
             let ids = await self.fetchCarbs()
             let ids = await self.fetchCarbs()
-            await updateCarbsArray(with: ids)
+            let carbObjects: [CarbEntryStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
+            await updateCarbsArray(with: carbObjects)
         }
         }
     }
     }
 
 
@@ -656,24 +653,16 @@ extension Home.StateModel {
         }
         }
     }
     }
 
 
-    @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
     // Setup FPUs
     private func setupFPUsArray() {
     private func setupFPUsArray() {
         Task {
         Task {
             let ids = await self.fetchFPUs()
             let ids = await self.fetchFPUs()
-            await updateFPUsArray(with: ids)
+            let fpuObjects: [CarbEntryStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
+            await updateFPUsArray(with: fpuObjects)
         }
         }
     }
     }
 
 
@@ -693,17 +682,8 @@ extension Home.StateModel {
         }
         }
     }
     }
 
 
-    @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
     // Custom fetch to more efficiently filter only for cob and iob
@@ -730,6 +710,7 @@ extension Home.StateModel {
     // Setup Determinations
     // Setup Determinations
     private func setupDeterminationsArray() {
     private func setupDeterminationsArray() {
         Task {
         Task {
+            // Get the NSManagedObjectIDs
             async let enactedObjectIDs = determinationStorage
             async let enactedObjectIDs = determinationStorage
                 .fetchLastDeterminationObjectID(predicate: NSPredicate.enactedDetermination)
                 .fetchLastDeterminationObjectID(predicate: NSPredicate.enactedDetermination)
             async let enactedAndNonEnactedObjectIDs = fetchCobAndIob()
             async let enactedAndNonEnactedObjectIDs = fetchCobAndIob()
@@ -737,14 +718,10 @@ extension Home.StateModel {
             let enactedIDs = await enactedObjectIDs
             let enactedIDs = await enactedObjectIDs
             let enactedAndNonEnactedIDs = await enactedAndNonEnactedObjectIDs
             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()
             await updateForecastData()
         }
         }
     }
     }
@@ -752,24 +729,21 @@ extension Home.StateModel {
     @MainActor private func updateDeterminationsArray(
     @MainActor private func updateDeterminationsArray(
         with IDs: [NSManagedObjectID],
         with IDs: [NSManagedObjectID],
         keyPath: ReferenceWritableKeyPath<Home.StateModel, [OrefDetermination]>
         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
     // Setup Insulin
     private func setupInsulinArray() {
     private func setupInsulinArray() {
         Task {
         Task {
             let ids = await self.fetchInsulin()
             let ids = await self.fetchInsulin()
-            await updateInsulinArray(with: ids)
+            let insulinObjects: [PumpEventStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
+            await updateInsulinArray(with: insulinObjects)
         }
         }
     }
     }
 
 
@@ -791,31 +765,21 @@ extension Home.StateModel {
         }
         }
     }
     }
 
 
-    @MainActor private func updateInsulinArray(with IDs: [NSManagedObjectID]) {
-        do {
-            let insulinObjects = try IDs.compactMap { id in
-                try viewContext.existingObject(with: id) as? PumpEventStored
-            }
-            insulinFromPersistence = insulinObjects
+    @MainActor private func updateInsulinArray(with insulinObjects: [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
-
-            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
     // Setup Last Bolus to display the bolus progress bar
@@ -858,7 +822,8 @@ extension Home.StateModel {
     private func setupBatteryArray() {
     private func setupBatteryArray() {
         Task {
         Task {
             let ids = await self.fetchBattery()
             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)
         }
         }
     }
     }
 
 
@@ -878,17 +843,8 @@ extension Home.StateModel {
         }
         }
     }
     }
 
 
-    @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
     }
     }
 }
 }
 
 
@@ -897,7 +853,8 @@ extension Home.StateModel {
     private func setupOverrides() {
     private func setupOverrides() {
         Task {
         Task {
             let ids = await self.fetchOverrides()
             let ids = await self.fetchOverrides()
-            await updateOverrideArray(with: ids)
+            let overrideObjects: [OverrideStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
+            await updateOverrideArray(with: overrideObjects)
         }
         }
     }
     }
 
 
@@ -917,18 +874,8 @@ extension Home.StateModel {
         }
         }
     }
     }
 
 
-    @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 {
     @MainActor func calculateDuration(override: OverrideStored) -> TimeInterval {
@@ -949,7 +896,9 @@ extension Home.StateModel {
     private func setupOverrideRunStored() {
     private func setupOverrideRunStored() {
         Task {
         Task {
             let ids = await self.fetchOverrideRunStored()
             let ids = await self.fetchOverrideRunStored()
-            await updateOverrideRunStoredArray(with: ids)
+            let overrideRunObjects: [OverrideRunStored] = await CoreDataStack.shared
+                .getNSManagedObject(with: ids, context: viewContext)
+            await updateOverrideRunStoredArray(with: overrideRunObjects)
         }
         }
     }
     }
 
 
@@ -970,19 +919,8 @@ extension Home.StateModel {
         }
         }
     }
     }
 
 
-    @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 {
     @MainActor func saveToOverrideRunStored(withID id: NSManagedObjectID) async {
@@ -1006,41 +944,125 @@ extension Home.StateModel {
     }
     }
 }
 }
 
 
-// MARK: Extension for Main Chart to draw Forecasts
-
 extension Home.StateModel {
 extension Home.StateModel {
+    // Asynchronously preprocess forecast data in a background thread
     func preprocessForecastData() async -> [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])] {
     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)
+                    }
+                }
 
 
-        // Get forecast and forecast values
-        let forecastIDs = await determinationStorage.getForecastIDs(for: id, in: context)
-        var result: [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])] = []
+                // Collect the results from the task group
+                for await (uuid, forecastID, forecastValueIDs) in group {
+                    result.append((id: uuid, forecastID: forecastID, forecastValueIDs: forecastValueIDs))
+                }
+            }
 
 
-        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))
+            return result
+        }.value
+    }
+
+    // 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] = []
+
+        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 {
     @MainActor func updateForecastData() async {
+        // Preprocess forecast data on a background thread
         let forecastData = await preprocessForecastData()
         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
     }
     }
 }
 }

+ 308 - 89
FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift

@@ -58,8 +58,9 @@ struct MainChartView: View {
     @State private var basalProfiles: [BasalProfile] = []
     @State private var basalProfiles: [BasalProfile] = []
     @State private var chartTempTargets: [ChartTempTarget] = []
     @State private var chartTempTargets: [ChartTempTarget] = []
     @State private var count: Decimal = 1
     @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 minValue: Decimal = 45
     @State private var maxValue: Decimal = 270
     @State private var maxValue: Decimal = 270
     @State private var selection: Date? = nil
     @State private var selection: Date? = nil
@@ -91,10 +92,6 @@ struct MainChartView: View {
         return formatter
         return formatter
     }
     }
 
 
-    private var conversionFactor: Decimal {
-        units == .mmolL ? 0.0555 : 1
-    }
-
     private var upperLimit: Decimal {
     private var upperLimit: Decimal {
         units == .mgdL ? 400 : 22.2
         units == .mgdL ? 400 : 22.2
     }
     }
@@ -107,10 +104,6 @@ struct MainChartView: View {
         units == .mgdL ? 30 : 1.66
         units == .mgdL ? 30 : 1.66
     }
     }
 
 
-    private var interpolationFactor: Double {
-        Double(state.enactedAndNonEnactedDeterminations.first?.cob ?? 1) * 10
-    }
-
     private var selectedGlucose: GlucoseStored? {
     private var selectedGlucose: GlucoseStored? {
         if let selection = selection {
         if let selection = selection {
             let lowerBound = selection.addingTimeInterval(-120)
             let lowerBound = selection.addingTimeInterval(-120)
@@ -121,6 +114,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 {
     var body: some View {
         VStack {
         VStack {
             ZStack {
             ZStack {
@@ -199,6 +216,29 @@ extension Backport {
             content
             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 {
 extension MainChartView {
@@ -207,9 +247,9 @@ extension MainChartView {
         Chart {
         Chart {
             /// high and low threshold lines
             /// high and low threshold lines
             if thresholdLines {
             if thresholdLines {
-                RuleMark(y: .value("High", highGlucose * conversionFactor)).foregroundStyle(Color.loopYellow)
+                RuleMark(y: .value("High", highGlucose)).foregroundStyle(Color.loopYellow)
                     .lineStyle(.init(lineWidth: 1, dash: [5]))
                     .lineStyle(.init(lineWidth: 1, dash: [5]))
-                RuleMark(y: .value("Low", lowGlucose * conversionFactor)).foregroundStyle(Color.loopRed)
+                RuleMark(y: .value("Low", lowGlucose)).foregroundStyle(Color.loopRed)
                     .lineStyle(.init(lineWidth: 1, dash: [5]))
                     .lineStyle(.init(lineWidth: 1, dash: [5]))
             }
             }
         }
         }
@@ -220,7 +260,7 @@ extension MainChartView {
         .chartXScale(domain: startMarker ... endMarker)
         .chartXScale(domain: startMarker ... endMarker)
         .chartXAxis(.hidden)
         .chartXAxis(.hidden)
         .chartYAxis { mainChartYAxis }
         .chartYAxis { mainChartYAxis }
-        .chartYScale(domain: minValue ... maxValue)
+        .chartYScale(domain: units == .mgdL ? minValue ... maxValue : minValue.asMmolL ... maxValue.asMmolL)
         .chartLegend(.hidden)
         .chartLegend(.hidden)
     }
     }
 
 
@@ -262,20 +302,54 @@ extension MainChartView {
                 drawTempTargets()
                 drawTempTargets()
                 drawActiveOverrides()
                 drawActiveOverrides()
                 drawOverrideRunStored()
                 drawOverrideRunStored()
-                drawForecasts()
                 drawGlucose(dummy: false)
                 drawGlucose(dummy: false)
                 drawManualGlucose()
                 drawManualGlucose()
                 drawCarbs()
                 drawCarbs()
 
 
+                if state.displayForecastsAsLines {
+                    drawForecastsLines()
+                } else {
+                    drawForecastsCone()
+                }
+
                 /// show glucose value when hovering over it
                 /// 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")
             .id("MainChart")
@@ -295,37 +369,57 @@ extension MainChartView {
             .chartYAxis { mainChartYAxis }
             .chartYAxis { mainChartYAxis }
             .chartYAxis(.hidden)
             .chartYAxis(.hidden)
             .backport.chartXSelection(value: $selection)
             .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)
         }
         }
     }
     }
 
 
     @ViewBuilder var selectionPopover: some View {
     @ViewBuilder var selectionPopover: some View {
         if let sgv = selectedGlucose?.glucose {
         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 {
                 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 {
             .background {
                 RoundedRectangle(cornerRadius: 4)
                 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)
+                    )
             }
             }
         }
         }
     }
     }
@@ -359,8 +453,7 @@ extension MainChartView {
             .chartXAxis { basalChartXAxis }
             .chartXAxis { basalChartXAxis }
             .chartXAxis(.hidden)
             .chartXAxis(.hidden)
             .chartYAxis(.hidden)
             .chartYAxis(.hidden)
-            .rotationEffect(.degrees(180))
-            .scaleEffect(x: -1, y: 1, anchor: .center)
+            .chartPlotStyle { basalChartPlotStyle($0) }
         }
         }
     }
     }
 
 
@@ -368,10 +461,29 @@ extension MainChartView {
         VStack {
         VStack {
             Chart {
             Chart {
                 drawIOB()
                 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(minHeight: geo.size.height * 0.12)
             .frame(width: fullWidth(viewWidth: screenSize.width))
             .frame(width: fullWidth(viewWidth: screenSize.width))
             .chartXScale(domain: startMarker ... endMarker)
             .chartXScale(domain: startMarker ... endMarker)
+            .backport.chartXSelection(value: $selection)
             .chartXAxis { basalChartXAxis }
             .chartXAxis { basalChartXAxis }
             .chartYAxis { cobChartYAxis }
             .chartYAxis { cobChartYAxis }
             .chartYScale(domain: minValueIobChart ... maxValueIobChart)
             .chartYScale(domain: minValueIobChart ... maxValueIobChart)
@@ -383,10 +495,29 @@ extension MainChartView {
         Chart {
         Chart {
             drawCurrentTimeMarker()
             drawCurrentTimeMarker()
             drawCOB(dummy: false)
             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(minHeight: geo.size.height * 0.12)
         .frame(width: fullWidth(viewWidth: screenSize.width))
         .frame(width: fullWidth(viewWidth: screenSize.width))
         .chartXScale(domain: startMarker ... endMarker)
         .chartXScale(domain: startMarker ... endMarker)
+        .backport.chartXSelection(value: $selection)
         .chartXAxis { basalChartXAxis }
         .chartXAxis { basalChartXAxis }
         .chartYAxis { cobChartYAxis }
         .chartYAxis { cobChartYAxis }
         .chartYScale(domain: minValueCobChart ... maxValueCobChart)
         .chartYScale(domain: minValueCobChart ... maxValueCobChart)
@@ -402,7 +533,7 @@ extension MainChartView {
             let bolusDate = insulin.timestamp ?? Date()
             let bolusDate = insulin.timestamp ?? Date()
 
 
             if amount != 0, let glucose = timeToNearestGlucose(time: bolusDate.timeIntervalSince1970)?.glucose {
             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
                 let size = (Config.bolusSize + CGFloat(truncating: amount) * Config.bolusScale) * 1.8
 
 
                 PointMark(
                 PointMark(
@@ -415,7 +546,7 @@ extension MainChartView {
                 .annotation(position: .top) {
                 .annotation(position: .top) {
                     Text(bolusFormatter.string(from: amount) ?? "")
                     Text(bolusFormatter.string(from: amount) ?? "")
                         .font(.caption2)
                         .font(.caption2)
-                        .foregroundStyle(Color.insulin)
+                        .foregroundStyle(Color.primary)
                 }
                 }
             }
             }
         }
         }
@@ -428,20 +559,21 @@ extension MainChartView {
             let carbDate = carb.date ?? Date()
             let carbDate = carb.date ?? Date()
 
 
             if let glucose = timeToNearestGlucose(time: carbDate.timeIntervalSince1970)?.glucose {
             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 size = (Config.carbsSize + CGFloat(carbAmount) * Config.carbsScale)
+                let limitedSize = size > 30 ? 30 : size
 
 
                 PointMark(
                 PointMark(
                     x: .value("Time", carbDate, unit: .second),
                     x: .value("Time", carbDate, unit: .second),
                     y: .value("Value", yPosition)
                     y: .value("Value", yPosition)
                 )
                 )
                 .symbol {
                 .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))
                         .rotationEffect(.degrees(180))
                 }
                 }
                 .annotation(position: .bottom) {
                 .annotation(position: .bottom) {
                     Text(carbsFormatter.string(from: carbAmount as NSNumber)!).font(.caption2)
                     Text(carbsFormatter.string(from: carbAmount as NSNumber)!).font(.caption2)
-                        .foregroundStyle(Color.orange)
+                        .foregroundStyle(Color.primary)
                 }
                 }
             }
             }
         }
         }
@@ -463,42 +595,66 @@ extension MainChartView {
         }
         }
     }
     }
 
 
+    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 {
     private func drawGlucose(dummy _: Bool) -> some ChartContent {
         /// glucose point mark
         /// glucose point mark
         /// filtering for high and low bounds in settings
         /// filtering for high and low bounds in settings
         ForEach(state.glucoseFromPersistence) { item in
         ForEach(state.glucoseFromPersistence) { item in
+            let glucoseToDisplay = units == .mgdL ? Decimal(item.glucose) : Decimal(item.glucose).asMmolL
+
             if smooth {
             if smooth {
-                if item.glucose > Int(highGlucose) {
-                    PointMark(
-                        x: .value("Time", item.date ?? Date(), unit: .second),
-                        y: .value("Value", Decimal(item.glucose) * conversionFactor)
-                    ).foregroundStyle(Color.orange.gradient).symbolSize(20).interpolationMethod(.cardinal)
-                } else if item.glucose < Int(lowGlucose) {
-                    PointMark(
-                        x: .value("Time", item.date ?? Date(), unit: .second),
-                        y: .value("Value", Decimal(item.glucose) * conversionFactor)
-                    ).foregroundStyle(Color.red.gradient).symbolSize(20).interpolationMethod(.cardinal)
-                } else {
-                    PointMark(
-                        x: .value("Time", item.date ?? Date(), unit: .second),
-                        y: .value("Value", Decimal(item.glucose) * conversionFactor)
-                    ).foregroundStyle(Color.green.gradient).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 {
             } else {
-                if item.glucose > Int(highGlucose) {
+                if glucoseToDisplay > highGlucose {
                     PointMark(
                     PointMark(
                         x: .value("Time", item.date ?? Date(), unit: .second),
                         x: .value("Time", item.date ?? Date(), unit: .second),
-                        y: .value("Value", Decimal(item.glucose) * conversionFactor)
+                        y: .value("Value", glucoseToDisplay)
                     ).foregroundStyle(Color.orange.gradient).symbolSize(20)
                     ).foregroundStyle(Color.orange.gradient).symbolSize(20)
-                } else if item.glucose < Int(lowGlucose) {
+                } else if glucoseToDisplay < lowGlucose {
                     PointMark(
                     PointMark(
                         x: .value("Time", item.date ?? Date(), unit: .second),
                         x: .value("Time", item.date ?? Date(), unit: .second),
-                        y: .value("Value", Decimal(item.glucose) * conversionFactor)
+                        y: .value("Value", glucoseToDisplay)
                     ).foregroundStyle(Color.red.gradient).symbolSize(20)
                     ).foregroundStyle(Color.red.gradient).symbolSize(20)
                 } else {
                 } else {
                     PointMark(
                     PointMark(
                         x: .value("Time", item.date ?? Date(), unit: .second),
                         x: .value("Time", item.date ?? Date(), unit: .second),
-                        y: .value("Value", Decimal(item.glucose) * conversionFactor)
+                        y: .value("Value", glucoseToDisplay)
                     ).foregroundStyle(Color.green.gradient).symbolSize(20)
                     ).foregroundStyle(Color.green.gradient).symbolSize(20)
                 }
                 }
             }
             }
@@ -511,18 +667,66 @@ extension MainChartView {
         return currentTime.addingTimeInterval(timeInterval)
         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
         ForEach(state.preprocessedData, id: \.id) { tuple in
             let forecastValue = tuple.forecastValue
             let forecastValue = tuple.forecastValue
             let forecast = tuple.forecast
             let forecast = tuple.forecast
             let valueAsDecimal = Decimal(forecastValue.value)
             let valueAsDecimal = Decimal(forecastValue.value)
             let displayValue = units == .mmolL ? valueAsDecimal.asMmolL : valueAsDecimal
             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 ?? ""))
+            }
         }
         }
     }
     }
 
 
@@ -615,10 +819,10 @@ extension MainChartView {
     private func drawManualGlucose() -> some ChartContent {
     private func drawManualGlucose() -> some ChartContent {
         /// manual glucose mark
         /// manual glucose mark
         ForEach(state.manualGlucoseFromPersistence) { item in
         ForEach(state.manualGlucoseFromPersistence) { item in
-            let manualGlucose = item.glucose
+            let manualGlucose = units == .mgdL ? Decimal(item.glucose) : Decimal(item.glucose).asMmolL
             PointMark(
             PointMark(
                 x: .value("Time", item.date ?? Date(), unit: .second),
                 x: .value("Time", item.date ?? Date(), unit: .second),
-                y: .value("Value", Decimal(manualGlucose) * conversionFactor)
+                y: .value("Value", manualGlucose)
             )
             )
             .symbol {
             .symbol {
                 Image(systemName: "drop.fill").font(.system(size: 10)).symbolRenderingMode(.monochrome)
                 Image(systemName: "drop.fill").font(.system(size: 10)).symbolRenderingMode(.monochrome)
@@ -659,7 +863,8 @@ extension MainChartView {
 
 
     private func drawIOB() -> some ChartContent {
     private func drawIOB() -> some ChartContent {
         ForEach(state.enactedAndNonEnactedDeterminations) { iob in
         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()
             let date: Date = iob.deliverAt ?? Date()
 
 
             LineMark(x: .value("Time", date), y: .value("Amount", amount))
             LineMark(x: .value("Time", date), y: .value("Amount", amount))
@@ -876,9 +1081,10 @@ extension MainChartView {
             isTempTargetActive = firstNonZeroTarget.createdAt <= now && now <= end
             isTempTargetActive = firstNonZeroTarget.createdAt <= now && now <= end
 
 
             if firstNonZeroTarget.targetTop != nil {
             if firstNonZeroTarget.targetTop != nil {
+                let targetTop = firstNonZeroTarget.targetTop ?? 0
                 calculatedTTs
                 calculatedTTs
                     .append(ChartTempTarget(
                     .append(ChartTempTarget(
-                        amount: (firstNonZeroTarget.targetTop ?? 0) * conversionFactor,
+                        amount: units == .mgdL ? targetTop : targetTop.asMmolL,
                         start: firstNonZeroTarget.createdAt,
                         start: firstNonZeroTarget.createdAt,
                         end: end
                         end: end
                     ))
                     ))
@@ -949,7 +1155,18 @@ extension MainChartView {
     /// update start and  end marker to fix scroll update problem with x axis
     /// update start and  end marker to fix scroll update problem with x axis
     private func updateStartEndMarkers() {
     private func updateStartEndMarkers() {
         startMarker = Date(timeIntervalSince1970: TimeInterval(NSDate().timeIntervalSince1970 - 86400))
         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() {
     private func calculateBasals() {
@@ -991,16 +1208,19 @@ extension MainChartView {
               let minForecast = forecastValues.min(), let maxForecast = forecastValues.max()
               let minForecast = forecastValues.min(), let maxForecast = forecastValues.max()
         else {
         else {
             // default values
             // default values
-            minValue = 45 * conversionFactor - 20 * conversionFactor
-            maxValue = 270 * conversionFactor + 50 * conversionFactor
+            minValue = 45 - 20
+            maxValue = 270 + 50
             return
             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)
+
+        var minOverall = min(minGlucose, minForecast)
+        var maxOverall = max(maxGlucose, adjustedMaxForecast)
 
 
-        minValue = minOverall * conversionFactor - 50 * conversionFactor
-        maxValue = maxOverall * conversionFactor + 80 * conversionFactor
+        minValue = minOverall - 50
+        maxValue = maxOverall + 80
     }
     }
 
 
     private func yAxisChartDataCobChart() {
     private func yAxisChartDataCobChart() {
@@ -1033,7 +1253,6 @@ extension MainChartView {
         plotContent
         plotContent
             .rotationEffect(.degrees(180))
             .rotationEffect(.degrees(180))
             .scaleEffect(x: -1, y: 1)
             .scaleEffect(x: -1, y: 1)
-            .chartXAxis(.hidden)
     }
     }
 
 
     private var mainChartXAxis: some AxisContent {
     private var mainChartXAxis: some AxisContent {

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

@@ -84,13 +84,13 @@ struct CurrentGlucoseView: View {
                 VStack(alignment: .center) {
                 VStack(alignment: .center) {
                     HStack {
                     HStack {
                         if let glucoseValue = combinedGlucoseValues.first?.glucose {
                         if let glucoseValue = combinedGlucoseValues.first?.glucose {
-                            let displayGlucose = convertGlucose(glucoseValue, to: units)
+                            let displayGlucose = units == .mgdL ? Decimal(glucoseValue).description : Decimal(glucoseValue)
+                                .formattedAsMmolL
                             Text(
                             Text(
-                                glucoseValue == 400 ? "HIGH" :
-                                    glucoseFormatter.string(from: NSNumber(value: displayGlucose)) ?? "--"
+                                glucoseValue == 400 ? "HIGH" : displayGlucose
                             )
                             )
                             .font(.system(size: 40, weight: .bold, design: .rounded))
                             .font(.system(size: 40, weight: .bold, design: .rounded))
-                            .foregroundColor(alarm == nil ? colourGlucoseText : .loopRed)
+                            .foregroundColor(alarm == nil ? glucoseDisplayColor : .loopRed)
                         } else {
                         } else {
                             Text("--")
                             Text("--")
                                 .font(.system(size: 40, weight: .bold, design: .rounded))
                                 .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 {
     private var delta: String {
         guard combinedGlucoseValues.count >= 2 else {
         guard combinedGlucoseValues.count >= 2 else {
             return "--"
             return "--"
@@ -176,13 +167,17 @@ struct CurrentGlucoseView: View {
         return deltaFormatter.string(from: deltaAsDecimal as NSNumber) ?? "--"
         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
         // Fetch the first glucose reading and convert it to Int for comparison
         let whichGlucose = Int(combinedGlucoseValues.first?.glucose ?? 0)
         let whichGlucose = Int(combinedGlucoseValues.first?.glucose ?? 0)
 
 
         // Define default color based on the color scheme
         // Define default color based on the color scheme
         let defaultColor: Color = colorScheme == .dark ? .white : .black
         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
         // Ensure the thresholds are logical
         guard lowGlucose < highGlucose else { return .primary }
         guard lowGlucose < highGlucose else { return .primary }
 
 

+ 36 - 24
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -697,7 +697,7 @@ extension Home {
                         Spacer()
                         Spacer()
 
 
                         Button {
                         Button {
-                            state.waitForSuggestion = true
+                            state.showProgressView()
                             state.cancelBolus()
                             state.cancelBolus()
                         } label: {
                         } label: {
                             Image(systemName: "xmark.app")
                             Image(systemName: "xmark.app")
@@ -787,30 +787,42 @@ extension Home {
                     .font(.subheadline)
                     .font(.subheadline)
                     .foregroundColor(.secondary)
                     .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() }
                     Button { state.isLegendPresented.toggle() }
                     label: { Text("Got it!").frame(maxWidth: .infinity, alignment: .center) }
                     label: { Text("Got it!").frame(maxWidth: .infinity, alignment: .center) }

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

@@ -51,17 +51,6 @@ extension OverrideConfig {
             return formatter
             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 {
         var body: some View {
             VStack {
             VStack {
                 Picker("Tab", selection: $state.selectedTab) {
                 Picker("Tab", selection: $state.selectedTab) {
@@ -449,8 +438,8 @@ extension OverrideConfig {
         }
         }
 
 
         @ViewBuilder private func overridesView(for preset: OverrideStored) -> some View {
         @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 duration = (preset.duration ?? 0) as Decimal
             let name = ((preset.name ?? "") == "") || (preset.name?.isEmpty ?? true) ? "" : preset.name!
             let name = ((preset.name ?? "") == "") || (preset.name?.isEmpty ?? true) ? "" : preset.name!
             let percent = preset.percentage / 100
             let percent = preset.percentage / 100
@@ -458,7 +447,7 @@ extension OverrideConfig {
             let durationString = perpetual ? "" : "\(formatter.string(from: duration as NSNumber)!)"
             let durationString = perpetual ? "" : "\(formatter.string(from: duration as NSNumber)!)"
             let scheduledSMBstring = (preset.smbIsOff && preset.smbIsAlwaysOff) ? "Scheduled SMBs" : ""
             let scheduledSMBstring = (preset.smbIsOff && preset.smbIsAlwaysOff) ? "Scheduled SMBs" : ""
             let smbString = (preset.smbIsOff && scheduledSMBstring == "") ? "SMBs are off" : ""
             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 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 maxMinutesUAM = (preset.uamMinutes as Decimal?) != nil ? (preset.uamMinutes ?? 0) as Decimal : 0
             let isfString = preset.isf ? "ISF" : ""
             let isfString = preset.isf ? "ISF" : ""

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

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

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

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

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

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

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

@@ -79,32 +79,20 @@ extension LiveActivityAttributes.ContentState {
 
 
         switch settings.lockScreenView {
         switch settings.lockScreenView {
         case .detailed:
         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)
             let chartDate = chart.map(\.date)
 
 
             /// glucose limits from UI settings, not from notifications settings
             /// 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(
             detailedState = LiveActivityAttributes.ContentAdditionalState(
-                chart: convertedChartBG,
+                chart: chartBG,
                 chartDate: chartDate,
                 chartDate: chartDate,
                 rotationDegrees: rotationDegrees,
                 rotationDegrees: rotationDegrees,
-                highGlucose: Double(highGlucose),
-                lowGlucose: Double(lowGlucose),
-                cob: Decimal(cob),
-                iob: iob as Decimal,
-                unit: unit,
-                isOverrideActive: isOverrideActive
+                highGlucose: settings.high,
+                lowGlucose: settings.low,
+                cob: Decimal(determination?.cob ?? 0),
+                iob: determination?.iob ?? 0 as Decimal,
+                unit: settings.units.rawValue,
+                isOverrideActive: override?.isActive ?? false
             )
             )
 
 
         case .simple:
         case .simple:

+ 103 - 95
FreeAPS/Sources/Services/WatchManager/WatchManager.swift

@@ -67,6 +67,7 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
         setupNotification()
         setupNotification()
         coreDataObserver = CoreDataObserver()
         coreDataObserver = CoreDataObserver()
         registerHandlers()
         registerHandlers()
+
         Task {
         Task {
             await configureState()
             await configureState()
         }
         }
@@ -91,10 +92,6 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
             }
             }
             return data
             return data
         }
         }
-
-        Task {
-            await configureState()
-        }
     }
     }
 
 
     func setupNotification() {
     func setupNotification() {
@@ -190,111 +187,122 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
         }
         }
     }
     }
 
 
-    @MainActor private func configureState() async {
+    private func configureState() async {
         let glucoseValuesIDs = await fetchGlucose()
         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 {
         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 lastDetermination = try viewContext.existingObject(with: lastDeterminationID) as? OrefDetermination
             let latestOverride = try viewContext.existingObject(with: latestOverrideID) as? OverrideStored
             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.directionEnum?.symbol
-                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 {
         } catch let error as NSError {
             debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to configure state with error: \(error)")
             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 textFieldDidBeginEditing: (() -> Void)?
     var numberFormatter: NumberFormatter
     var numberFormatter: NumberFormatter
     var allowDecimalSeparator: Bool
     var allowDecimalSeparator: Bool
+    var previousTextField: (() -> Void)?
+    var nextTextField: (() -> Void)?
 
 
     public init(
     public init(
         text: Binding<Decimal>,
         text: Binding<Decimal>,
@@ -29,7 +31,9 @@ public struct TextFieldWithToolBar: UIViewRepresentable {
         isDismissible: Bool = true,
         isDismissible: Bool = true,
         textFieldDidBeginEditing: (() -> Void)? = nil,
         textFieldDidBeginEditing: (() -> Void)? = nil,
         numberFormatter: NumberFormatter,
         numberFormatter: NumberFormatter,
-        allowDecimalSeparator: Bool = true
+        allowDecimalSeparator: Bool = true,
+        previousTextField: (() -> Void)? = nil,
+        nextTextField: (() -> Void)? = nil
     ) {
     ) {
         _text = text
         _text = text
         self.placeholder = placeholder
         self.placeholder = placeholder
@@ -45,6 +49,8 @@ public struct TextFieldWithToolBar: UIViewRepresentable {
         self.numberFormatter = numberFormatter
         self.numberFormatter = numberFormatter
         self.numberFormatter.numberStyle = .decimal
         self.numberFormatter.numberStyle = .decimal
         self.allowDecimalSeparator = allowDecimalSeparator
         self.allowDecimalSeparator = allowDecimalSeparator
+        self.previousTextField = previousTextField
+        self.nextTextField = nextTextField
     }
     }
 
 
     public func makeUIView(context: Context) -> UITextField {
     public func makeUIView(context: Context) -> UITextField {
@@ -77,8 +83,20 @@ public struct TextFieldWithToolBar: UIViewRepresentable {
             target: context.coordinator,
             target: context.coordinator,
             action: #selector(Coordinator.clearText)
             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()
         toolbar.sizeToFit()
         return toolbar
         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
         // Helper method to calculate the number of decimal places in a string
         fileprivate func calculateDecimalPlaces(in string: String) -> Int {
         fileprivate func calculateDecimalPlaces(in string: String) -> Int {
             guard let decimalSeparator = decimalFormatter.decimalSeparator else { return 0 }
             guard let decimalSeparator = decimalFormatter.decimalSeparator else { return 0 }

+ 64 - 9
LiveActivity/LiveActivity.swift

@@ -1,5 +1,6 @@
 import ActivityKit
 import ActivityKit
 import Charts
 import Charts
+import Foundation
 import SwiftUI
 import SwiftUI
 import WidgetKit
 import WidgetKit
 
 
@@ -9,6 +10,55 @@ private enum Size {
     case expanded
     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 {
 struct LiveActivity: Widget {
     private let dateFormatter: DateFormatter = {
     private let dateFormatter: DateFormatter = {
         var f = DateFormatter()
         var f = DateFormatter()
@@ -215,27 +265,32 @@ struct LiveActivity: Widget {
             Text("No data available")
             Text("No data available")
         } else {
         } else {
             // Determine scale
             // 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 {
             Chart {
-                RuleMark(y: .value("High", additionalState.highGlucose))
+                RuleMark(y: .value("Low", yAxisRuleMarkMin))
                     .lineStyle(.init(lineWidth: 0.5, dash: [5]))
                     .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]))
                     .lineStyle(.init(lineWidth: 0.5, dash: [5]))
 
 
                 ForEach(additionalState.chart.indices, id: \.self) { index in
                 ForEach(additionalState.chart.indices, id: \.self) { index in
                     let currentValue = additionalState.chart[index]
                     let currentValue = additionalState.chart[index]
+                    let displayValue = additionalState.unit == "mg/dL" ? currentValue : currentValue.asMmolL
                     let chartDate = additionalState.chartDate[index] ?? Date()
                     let chartDate = additionalState.chartDate[index] ?? Date()
                     let pointMark = PointMark(
                     let pointMark = PointMark(
                         x: .value("Time", chartDate),
                         x: .value("Time", chartDate),
-                        y: .value("Value", currentValue)
+                        y: .value("Value", displayValue)
                     ).symbolSize(15)
                     ).symbolSize(15)
 
 
-                    if currentValue > additionalState.highGlucose {
+                    if displayValue > yAxisRuleMarkMax {
                         pointMark.foregroundStyle(Color.orange.gradient)
                         pointMark.foregroundStyle(Color.orange.gradient)
-                    } else if currentValue < additionalState.lowGlucose {
+                    } else if displayValue < yAxisRuleMarkMin {
                         pointMark.foregroundStyle(Color.red.gradient)
                         pointMark.foregroundStyle(Color.red.gradient)
                     } else {
                     } else {
                         pointMark.foregroundStyle(Color.green.gradient)
                         pointMark.foregroundStyle(Color.green.gradient)
@@ -248,7 +303,7 @@ struct LiveActivity: Widget {
                     AxisValueLabel().foregroundStyle(.secondary).font(.footnote)
                     AxisValueLabel().foregroundStyle(.secondary).font(.footnote)
                 }
                 }
             }
             }
-            .chartYScale(domain: min ... max)
+            .chartYScale(domain: additionalState.unit == "mg/dL" ? min ... max : min.asMmolL ... max.asMmolL)
             .chartXAxis {
             .chartXAxis {
                 AxisMarks(position: .automatic) { _ in
                 AxisMarks(position: .automatic) { _ in
                     AxisGridLine(stroke: .init(lineWidth: 0.2, dash: [2, 3])).foregroundStyle(Color.white)
                     AxisGridLine(stroke: .init(lineWidth: 0.2, dash: [2, 3])).foregroundStyle(Color.white)

+ 20 - 0
Model/CoreDataStack.swift

@@ -403,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
 // MARK: - Save

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

+ 9 - 0
Model/Helper/NSPredicates.swift

@@ -30,6 +30,10 @@ extension Date {
         Calendar.current.date(byAdding: .hour, value: -2, to: 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 {
     static var sixHoursAgo: Date {
         Calendar.current.date(byAdding: .hour, value: -6, to: Date())!
         Calendar.current.date(byAdding: .hour, value: -6, to: Date())!
     }
     }
@@ -87,6 +91,11 @@ extension NSPredicate {
         return NSPredicate(format: "date >= %@", date as NSDate)
         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 {
     static var predicateForSixHoursAgo: NSPredicate {
         let date = Date.sixHoursAgo
         let date = Date.sixHoursAgo
         return NSPredicate(format: "date >= %@", date as NSDate)
         return NSPredicate(format: "date >= %@", date as NSDate)

+ 4 - 1
oref0_source_version.txt

@@ -1,6 +1,9 @@
-oref0 branch: dev - git version: d1dfb70
+oref0 branch: dev - git version: 363fd11
 
 
 Last commits:
 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
 d1dfb70 Merge pull request #26 from MikePlante1/typo
 d9f1662 fix `threshold_setting` typo
 d9f1662 fix `threshold_setting` typo
 b454837 Merge pull request #24 from nightscout/Trio_renames
 b454837 Merge pull request #24 from nightscout/Trio_renames

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

@@ -8,11 +8,11 @@ var _ = require('lodash');
 
 
 function defaults ( ) {
 function defaults ( ) {
   return /* profile */ {
   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
     , 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
     // , 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
     , 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
     , 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
     , 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
     // 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
     , 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
     // *** 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
     // 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
     // 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
     // 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_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_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.
     // *** 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.
     , 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
     , 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
     , curve: "rapid-acting" // change this to "ultra-rapid" for Fiasp, or "bilinear" for old curve
     , useCustomPeakTime: false // allows changing insulinPeakTime
     , 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
     , carbsReqThreshold: 1 // grams of carbsReq to trigger a pushover
     , offline_hotspot: false // enabled an offline-only local wifi hotspot if no Internet available
     , 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
     , 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
     //, maxRaw: 200 // highest raw/noisy CGM value considered safe to use for looping
     , calc_glucose_noise: false
     , calc_glucose_noise: false
     , target_bg: false // set to an integer value in mg/dL to override pump min_bg
     , 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.
     , 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
     , adjustmentFactor: 0.8
     , adjustmentFactorSigmoid: 0.5
     , adjustmentFactorSigmoid: 0.5
@@ -75,8 +76,6 @@ function defaults ( ) {
     , sigmoid: false
     , sigmoid: false
     , weightPercentage: 0.65 
     , weightPercentage: 0.65 
     , tddAdjBasal: false // Enable adjustment of basal based on the ratio of 24 h : 10 day average TDD
     , tddAdjBasal: false // Enable adjustment of basal based on the ratio of 24 h : 10 day average TDD
-    , enableSMB_high_bg: false // enable SMBs when a high BG is detected, based on the high BG target (adjusted or profile)
-    , enableSMB_high_bg_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
     , threshold_setting: 60 // Use a configurable threshold setting
   }
   }
 }
 }