Преглед изворни кода

Merge branch 'settings-update' of https://github.com/tmhastings/Trio-dev into settings-update

tmhastings пре 1 година
родитељ
комит
df608d82ef
41 измењених фајлова са 1191 додато и 53 уклоњено
  1. 1 1
      CODE_OF_CONDUCT.md
  2. 1 1
      Config.xcconfig
  3. 72 0
      FreeAPS.xcodeproj/project.pbxproj
  4. 2 0
      FreeAPS/Resources/FreeAPS.entitlements
  5. 2 0
      FreeAPS/Resources/Info.plist
  6. 11 1
      FreeAPS/Sources/APS/FetchGlucoseManager.swift
  7. 1 0
      FreeAPS/Sources/APS/OpenAPS/Constants.swift
  8. 76 7
      FreeAPS/Sources/APS/OpenAPS/JavaScriptWorker.swift
  9. 2 2
      FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift
  10. 28 0
      FreeAPS/Sources/APS/Storage/OverrideStorage.swift
  11. 51 1
      FreeAPS/Sources/Application/AppDelegate.swift
  12. 4 0
      FreeAPS/Sources/Logger/Logger.swift
  13. 12 0
      FreeAPS/Sources/Models/NightscoutStatus.swift
  14. 1 0
      FreeAPS/Sources/Models/PumpHistoryEvent.swift
  15. 141 0
      FreeAPS/Sources/Models/PushMessage.swift
  16. 7 7
      FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift
  17. 16 0
      FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift
  18. 9 0
      FreeAPS/Sources/Modules/OverrideConfig/OverrideStateModel.swift
  19. 1 1
      FreeAPS/Sources/Modules/OverrideConfig/View/AddOverrideForm.swift
  20. 4 2
      FreeAPS/Sources/Modules/OverrideConfig/View/EditOverrideForm.swift
  21. 36 0
      FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+APNS.swift
  22. 108 0
      FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+Bolus.swift
  23. 12 0
      FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+Helpers.swift
  24. 82 0
      FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+Meal.swift
  25. 100 0
      FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+Override.swift
  26. 49 0
      FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+TempTarget.swift
  27. 113 0
      FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl.swift
  28. 6 0
      FreeAPS/Sources/Modules/RemoteControlConfig/RemoteControlConfigDataFlow.swift
  29. 4 0
      FreeAPS/Sources/Modules/RemoteControlConfig/RemoteControlConfigProvider.swift
  30. 47 0
      FreeAPS/Sources/Modules/RemoteControlConfig/RemoteControlConfigStateModel.swift
  31. 113 0
      FreeAPS/Sources/Modules/RemoteControlConfig/View/RemoteControlConfig.swift
  32. 6 0
      FreeAPS/Sources/Modules/Settings/SettingItems.swift
  33. 1 0
      FreeAPS/Sources/Modules/Settings/View/Subviews/FeatureSettingsView.swift
  34. 3 0
      FreeAPS/Sources/Router/Screen.swift
  35. 44 9
      FreeAPS/Sources/Services/Network/NightscoutManager.swift
  36. 18 15
      FreeAPS/Sources/Services/WatchManager/WatchManager.swift
  37. 1 1
      FreeAPS/Sources/Shortcuts/AppShortcuts.swift
  38. 1 1
      FreeAPS/Sources/Shortcuts/Carbs/AddCarbPresetIntent.swift
  39. 2 2
      Model/TrioCoreDataPersistentContainer.xcdatamodeld/TrioCoreDataPersistentContainer.xcdatamodel/contents
  40. 1 1
      README.md
  41. 2 1
      fastlane/Fastfile

+ 1 - 1
CODE_OF_CONDUCT.md

@@ -59,7 +59,7 @@ representative at an online or offline event.
 ## Enforcement
 ## Enforcement
 
 
 Instances of abusive, harassing, or otherwise unacceptable behavior may be
 Instances of abusive, harassing, or otherwise unacceptable behavior may be
-reported to the Discord server admins. Please join our [Discord server](https://discord.gg/dbe5Twav8D) to contact
+reported to the Discord server admins. Please join our [Discord server](http://discord.diy-trio.org) to contact
 them directly for any enforcement issues. All complaints will be reviewed and
 them directly for any enforcement issues. All complaints will be reviewed and
 investigated promptly and fairly.
 investigated promptly and fairly.
 
 

+ 1 - 1
Config.xcconfig

@@ -1,5 +1,5 @@
 APP_DISPLAY_NAME = Trio
 APP_DISPLAY_NAME = Trio
-APP_VERSION = 0.2.0
+APP_VERSION = 0.3.0
 APP_BUILD_NUMBER = 1
 APP_BUILD_NUMBER = 1
 COPYRIGHT_NOTICE =
 COPYRIGHT_NOTICE =
 DEVELOPER_TEAM = ##TEAM_ID##
 DEVELOPER_TEAM = ##TEAM_ID##

+ 72 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -442,6 +442,12 @@
 		DD1745552C55CA6C00211FAC /* UnitsLimitsSettingsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1745542C55CA6C00211FAC /* UnitsLimitsSettingsRootView.swift */; };
 		DD1745552C55CA6C00211FAC /* UnitsLimitsSettingsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1745542C55CA6C00211FAC /* UnitsLimitsSettingsRootView.swift */; };
 		DD1DB7CC2BECCA1F0048B367 /* BuildDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1DB7CB2BECCA1F0048B367 /* BuildDetails.swift */; };
 		DD1DB7CC2BECCA1F0048B367 /* BuildDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1DB7CB2BECCA1F0048B367 /* BuildDetails.swift */; };
 		DD21FCB52C6952AD00AF2C25 /* DecimalPickerSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD21FCB42C6952AD00AF2C25 /* DecimalPickerSettings.swift */; };
 		DD21FCB52C6952AD00AF2C25 /* DecimalPickerSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD21FCB42C6952AD00AF2C25 /* DecimalPickerSettings.swift */; };
+		DD32CF982CC82463003686D6 /* TrioRemoteControl+Bolus.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD32CF972CC82460003686D6 /* TrioRemoteControl+Bolus.swift */; };
+		DD32CF9A2CC8247B003686D6 /* TrioRemoteControl+Meal.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD32CF992CC8246F003686D6 /* TrioRemoteControl+Meal.swift */; };
+		DD32CF9C2CC82499003686D6 /* TrioRemoteControl+TempTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD32CF9B2CC82495003686D6 /* TrioRemoteControl+TempTarget.swift */; };
+		DD32CF9E2CC824C5003686D6 /* TrioRemoteControl+Override.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD32CF9D2CC824C2003686D6 /* TrioRemoteControl+Override.swift */; };
+		DD32CFA02CC824D6003686D6 /* TrioRemoteControl+APNS.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD32CF9F2CC824D3003686D6 /* TrioRemoteControl+APNS.swift */; };
+		DD32CFA22CC824E2003686D6 /* TrioRemoteControl+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD32CFA12CC824E1003686D6 /* TrioRemoteControl+Helpers.swift */; };
 		DD68889D2C386E17006E3C44 /* NightscoutExercise.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD68889C2C386E17006E3C44 /* NightscoutExercise.swift */; };
 		DD68889D2C386E17006E3C44 /* NightscoutExercise.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD68889C2C386E17006E3C44 /* NightscoutExercise.swift */; };
 		DD6B7CB22C7B6F0800B75029 /* Rounding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CB12C7B6F0800B75029 /* Rounding.swift */; };
 		DD6B7CB22C7B6F0800B75029 /* Rounding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CB12C7B6F0800B75029 /* Rounding.swift */; };
 		DD6B7CB42C7B71F700B75029 /* ForecastDisplayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */; };
 		DD6B7CB42C7B71F700B75029 /* ForecastDisplayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */; };
@@ -452,6 +458,12 @@
 		DD88C8E22C50420800F2D558 /* DefinitionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD88C8E12C50420800F2D558 /* DefinitionRow.swift */; };
 		DD88C8E22C50420800F2D558 /* DefinitionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD88C8E12C50420800F2D558 /* DefinitionRow.swift */; };
 		DD940BAA2CA7585D000830A5 /* GlucoseColorScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */; };
 		DD940BAA2CA7585D000830A5 /* GlucoseColorScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */; };
 		DD940BAC2CA75889000830A5 /* DynamicGlucoseColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */; };
 		DD940BAC2CA75889000830A5 /* DynamicGlucoseColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */; };
+		DD9ECB682CA99F4500AA7C45 /* TrioRemoteControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB672CA99F4500AA7C45 /* TrioRemoteControl.swift */; };
+		DD9ECB6A2CA99F6C00AA7C45 /* PushMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB692CA99F6C00AA7C45 /* PushMessage.swift */; };
+		DD9ECB702CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6D2CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift */; };
+		DD9ECB712CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6E2CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift */; };
+		DD9ECB722CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6F2CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift */; };
+		DD9ECB742CA9A0C300AA7C45 /* RemoteControlConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB732CA9A0C300AA7C45 /* RemoteControlConfig.swift */; };
 		DDCEBF5B2CC1B76400DF4C36 /* LiveActivity+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCEBF5A2CC1B76400DF4C36 /* LiveActivity+Helper.swift */; };
 		DDCEBF5B2CC1B76400DF4C36 /* LiveActivity+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCEBF5A2CC1B76400DF4C36 /* LiveActivity+Helper.swift */; };
 		DDD163122C4C689900CD525A /* OverrideStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163112C4C689900CD525A /* OverrideStateModel.swift */; };
 		DDD163122C4C689900CD525A /* OverrideStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163112C4C689900CD525A /* OverrideStateModel.swift */; };
 		DDD163142C4C68D300CD525A /* OverrideProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163132C4C68D300CD525A /* OverrideProvider.swift */; };
 		DDD163142C4C68D300CD525A /* OverrideProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163132C4C68D300CD525A /* OverrideProvider.swift */; };
@@ -1109,6 +1121,12 @@
 		DD1745542C55CA6C00211FAC /* UnitsLimitsSettingsRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitsLimitsSettingsRootView.swift; sourceTree = "<group>"; };
 		DD1745542C55CA6C00211FAC /* UnitsLimitsSettingsRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitsLimitsSettingsRootView.swift; sourceTree = "<group>"; };
 		DD1DB7CB2BECCA1F0048B367 /* BuildDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildDetails.swift; sourceTree = "<group>"; };
 		DD1DB7CB2BECCA1F0048B367 /* BuildDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildDetails.swift; sourceTree = "<group>"; };
 		DD21FCB42C6952AD00AF2C25 /* DecimalPickerSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecimalPickerSettings.swift; sourceTree = "<group>"; };
 		DD21FCB42C6952AD00AF2C25 /* DecimalPickerSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecimalPickerSettings.swift; sourceTree = "<group>"; };
+		DD32CF972CC82460003686D6 /* TrioRemoteControl+Bolus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TrioRemoteControl+Bolus.swift"; sourceTree = "<group>"; };
+		DD32CF992CC8246F003686D6 /* TrioRemoteControl+Meal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TrioRemoteControl+Meal.swift"; sourceTree = "<group>"; };
+		DD32CF9B2CC82495003686D6 /* TrioRemoteControl+TempTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TrioRemoteControl+TempTarget.swift"; sourceTree = "<group>"; };
+		DD32CF9D2CC824C2003686D6 /* TrioRemoteControl+Override.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TrioRemoteControl+Override.swift"; sourceTree = "<group>"; };
+		DD32CF9F2CC824D3003686D6 /* TrioRemoteControl+APNS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TrioRemoteControl+APNS.swift"; sourceTree = "<group>"; };
+		DD32CFA12CC824E1003686D6 /* TrioRemoteControl+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TrioRemoteControl+Helpers.swift"; sourceTree = "<group>"; };
 		DD68889C2C386E17006E3C44 /* NightscoutExercise.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutExercise.swift; sourceTree = "<group>"; };
 		DD68889C2C386E17006E3C44 /* NightscoutExercise.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutExercise.swift; sourceTree = "<group>"; };
 		DD6B7CB12C7B6F0800B75029 /* Rounding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Rounding.swift; sourceTree = "<group>"; };
 		DD6B7CB12C7B6F0800B75029 /* Rounding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Rounding.swift; sourceTree = "<group>"; };
 		DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastDisplayType.swift; sourceTree = "<group>"; };
 		DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastDisplayType.swift; sourceTree = "<group>"; };
@@ -1119,6 +1137,12 @@
 		DD88C8E12C50420800F2D558 /* DefinitionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefinitionRow.swift; sourceTree = "<group>"; };
 		DD88C8E12C50420800F2D558 /* DefinitionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefinitionRow.swift; sourceTree = "<group>"; };
 		DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseColorScheme.swift; sourceTree = "<group>"; };
 		DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseColorScheme.swift; sourceTree = "<group>"; };
 		DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicGlucoseColor.swift; sourceTree = "<group>"; };
 		DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicGlucoseColor.swift; sourceTree = "<group>"; };
+		DD9ECB672CA99F4500AA7C45 /* TrioRemoteControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrioRemoteControl.swift; sourceTree = "<group>"; };
+		DD9ECB692CA99F6C00AA7C45 /* PushMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushMessage.swift; sourceTree = "<group>"; };
+		DD9ECB6D2CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigStateModel.swift; sourceTree = "<group>"; };
+		DD9ECB6E2CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigProvider.swift; sourceTree = "<group>"; };
+		DD9ECB6F2CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigDataFlow.swift; sourceTree = "<group>"; };
+		DD9ECB732CA9A0C300AA7C45 /* RemoteControlConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfig.swift; sourceTree = "<group>"; };
 		DDCEBF5A2CC1B76400DF4C36 /* LiveActivity+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LiveActivity+Helper.swift"; sourceTree = "<group>"; };
 		DDCEBF5A2CC1B76400DF4C36 /* LiveActivity+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LiveActivity+Helper.swift"; sourceTree = "<group>"; };
 		DDD163112C4C689900CD525A /* OverrideStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideStateModel.swift; sourceTree = "<group>"; };
 		DDD163112C4C689900CD525A /* OverrideStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideStateModel.swift; sourceTree = "<group>"; };
 		DDD163132C4C68D300CD525A /* OverrideProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideProvider.swift; sourceTree = "<group>"; };
 		DDD163132C4C68D300CD525A /* OverrideProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideProvider.swift; sourceTree = "<group>"; };
@@ -1477,6 +1501,8 @@
 		3811DE0325C9D31700A708ED /* Modules */ = {
 		3811DE0325C9D31700A708ED /* Modules */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				DD9ECB6B2CA99FA400AA7C45 /* RemoteControlConfig */,
+				DD9ECB662CA99EFE00AA7C45 /* RemoteControl */,
 				DD1745382C55BF8B00211FAC /* AlgorithmAdvancedSettings */,
 				DD1745382C55BF8B00211FAC /* AlgorithmAdvancedSettings */,
 				DD1745422C55C5C400211FAC /* AutosensSettings */,
 				DD1745422C55C5C400211FAC /* AutosensSettings */,
 				672F63EEAE27400625E14BAD /* AutotuneConfig */,
 				672F63EEAE27400625E14BAD /* AutotuneConfig */,
@@ -1969,6 +1995,7 @@
 				DD21FCB42C6952AD00AF2C25 /* DecimalPickerSettings.swift */,
 				DD21FCB42C6952AD00AF2C25 /* DecimalPickerSettings.swift */,
 				DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */,
 				DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */,
 				DD6B7CB52C7B748B00B75029 /* TotalInsulinDisplayType.swift */,
 				DD6B7CB52C7B748B00B75029 /* TotalInsulinDisplayType.swift */,
+				DD9ECB692CA99F6C00AA7C45 /* PushMessage.swift */,
 			);
 			);
 			path = Models;
 			path = Models;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -2705,6 +2732,39 @@
 			path = ProfileImport;
 			path = ProfileImport;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
 		};
 		};
+		DD9ECB662CA99EFE00AA7C45 /* RemoteControl */ = {
+			isa = PBXGroup;
+			children = (
+				DD32CFA12CC824E1003686D6 /* TrioRemoteControl+Helpers.swift */,
+				DD32CF9F2CC824D3003686D6 /* TrioRemoteControl+APNS.swift */,
+				DD32CF9D2CC824C2003686D6 /* TrioRemoteControl+Override.swift */,
+				DD32CF9B2CC82495003686D6 /* TrioRemoteControl+TempTarget.swift */,
+				DD32CF992CC8246F003686D6 /* TrioRemoteControl+Meal.swift */,
+				DD32CF972CC82460003686D6 /* TrioRemoteControl+Bolus.swift */,
+				DD9ECB672CA99F4500AA7C45 /* TrioRemoteControl.swift */,
+			);
+			path = RemoteControl;
+			sourceTree = "<group>";
+		};
+		DD9ECB6B2CA99FA400AA7C45 /* RemoteControlConfig */ = {
+			isa = PBXGroup;
+			children = (
+				DD9ECB6F2CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift */,
+				DD9ECB6E2CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift */,
+				DD9ECB6D2CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift */,
+				DD9ECB6C2CA99FAE00AA7C45 /* View */,
+			);
+			path = RemoteControlConfig;
+			sourceTree = "<group>";
+		};
+		DD9ECB6C2CA99FAE00AA7C45 /* View */ = {
+			isa = PBXGroup;
+			children = (
+				DD9ECB732CA9A0C300AA7C45 /* RemoteControlConfig.swift */,
+			);
+			path = View;
+			sourceTree = "<group>";
+		};
 		DDD163032C4C67B400CD525A /* OverrideConfig */ = {
 		DDD163032C4C67B400CD525A /* OverrideConfig */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
@@ -3201,6 +3261,7 @@
 				3811DEEB25CA063400A708ED /* PersistedProperty.swift in Sources */,
 				3811DEEB25CA063400A708ED /* PersistedProperty.swift in Sources */,
 				38E44537274E411700EC9A94 /* Disk+Helpers.swift in Sources */,
 				38E44537274E411700EC9A94 /* Disk+Helpers.swift in Sources */,
 				388E5A6025B6F2310019842D /* Autosens.swift in Sources */,
 				388E5A6025B6F2310019842D /* Autosens.swift in Sources */,
+				DD9ECB6A2CA99F6C00AA7C45 /* PushMessage.swift in Sources */,
 				3811DE8F25C9D80400A708ED /* User.swift in Sources */,
 				3811DE8F25C9D80400A708ED /* User.swift in Sources */,
 				19D466A329AA2B80004D5F33 /* MealSettingsDataFlow.swift in Sources */,
 				19D466A329AA2B80004D5F33 /* MealSettingsDataFlow.swift in Sources */,
 				3811DEB225C9D88300A708ED /* KeychainItemAccessibility.swift in Sources */,
 				3811DEB225C9D88300A708ED /* KeychainItemAccessibility.swift in Sources */,
@@ -3235,6 +3296,7 @@
 				DD1745402C55BFC100211FAC /* AlgorithmAdvancedSettingsRootView.swift in Sources */,
 				DD1745402C55BFC100211FAC /* AlgorithmAdvancedSettingsRootView.swift in Sources */,
 				58645BA52CA2D347008AFCE7 /* ForecastSetup.swift in Sources */,
 				58645BA52CA2D347008AFCE7 /* ForecastSetup.swift in Sources */,
 				110AEDEE2C51A0AE00615CC9 /* ShortcutsConfigStateModel.swift in Sources */,
 				110AEDEE2C51A0AE00615CC9 /* ShortcutsConfigStateModel.swift in Sources */,
+				DD9ECB722CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift in Sources */,
 				DD1745262C55526F00211FAC /* SMBSettingsRootView.swift in Sources */,
 				DD1745262C55526F00211FAC /* SMBSettingsRootView.swift in Sources */,
 				3811DE3025C9D49500A708ED /* HomeStateModel.swift in Sources */,
 				3811DE3025C9D49500A708ED /* HomeStateModel.swift in Sources */,
 				38BF021725E7CBBC00579895 /* PumpManagerExtensions.swift in Sources */,
 				38BF021725E7CBBC00579895 /* PumpManagerExtensions.swift in Sources */,
@@ -3292,7 +3354,9 @@
 				3811DF0225CA9FEA00A708ED /* Credentials.swift in Sources */,
 				3811DF0225CA9FEA00A708ED /* Credentials.swift in Sources */,
 				5837A5302BD2E3C700A5DC04 /* CarbEntryStored+helper.swift in Sources */,
 				5837A5302BD2E3C700A5DC04 /* CarbEntryStored+helper.swift in Sources */,
 				389A572026079BAA00BC102F /* Interpolation.swift in Sources */,
 				389A572026079BAA00BC102F /* Interpolation.swift in Sources */,
+				DD9ECB702CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift in Sources */,
 				19A910382A24EF3200C8951B /* ChartsView.swift in Sources */,
 				19A910382A24EF3200C8951B /* ChartsView.swift in Sources */,
+				DD32CF9A2CC8247B003686D6 /* TrioRemoteControl+Meal.swift in Sources */,
 				BDF34F832C10C5B600D51995 /* DataManager.swift in Sources */,
 				BDF34F832C10C5B600D51995 /* DataManager.swift in Sources */,
 				38B4F3C625E5017E00E76A18 /* NotificationCenter.swift in Sources */,
 				38B4F3C625E5017E00E76A18 /* NotificationCenter.swift in Sources */,
 				19D466A729AA2C22004D5F33 /* MealSettingsStateModel.swift in Sources */,
 				19D466A729AA2C22004D5F33 /* MealSettingsStateModel.swift in Sources */,
@@ -3322,6 +3386,7 @@
 				388358C825EEF6D200E024B2 /* BasalProfileEntry.swift in Sources */,
 				388358C825EEF6D200E024B2 /* BasalProfileEntry.swift in Sources */,
 				3811DE0B25C9D32F00A708ED /* BaseView.swift in Sources */,
 				3811DE0B25C9D32F00A708ED /* BaseView.swift in Sources */,
 				3811DE3225C9D49500A708ED /* HomeDataFlow.swift in Sources */,
 				3811DE3225C9D49500A708ED /* HomeDataFlow.swift in Sources */,
+				DD32CFA22CC824E2003686D6 /* TrioRemoteControl+Helpers.swift in Sources */,
 				CE1856F52ADC4858007E39C7 /* AddCarbPresetIntent.swift in Sources */,
 				CE1856F52ADC4858007E39C7 /* AddCarbPresetIntent.swift in Sources */,
 				38569347270B5DFB0002C50D /* CGMType.swift in Sources */,
 				38569347270B5DFB0002C50D /* CGMType.swift in Sources */,
 				3821ED4C25DD18BA00BC42AD /* Constants.swift in Sources */,
 				3821ED4C25DD18BA00BC42AD /* Constants.swift in Sources */,
@@ -3370,6 +3435,7 @@
 				CE7950242997D81700FA576E /* CGMSettingsView.swift in Sources */,
 				CE7950242997D81700FA576E /* CGMSettingsView.swift in Sources */,
 				58237D9E2BCF0A6B00A47A79 /* PopupView.swift in Sources */,
 				58237D9E2BCF0A6B00A47A79 /* PopupView.swift in Sources */,
 				38D0B3D925EC07C400CB6E88 /* CarbsEntry.swift in Sources */,
 				38D0B3D925EC07C400CB6E88 /* CarbsEntry.swift in Sources */,
+				DD32CF9C2CC82499003686D6 /* TrioRemoteControl+TempTarget.swift in Sources */,
 				38A9260525F012D8009E3739 /* CarbRatios.swift in Sources */,
 				38A9260525F012D8009E3739 /* CarbRatios.swift in Sources */,
 				38FCF3D625E8FDF40078B0D1 /* MD5.swift in Sources */,
 				38FCF3D625E8FDF40078B0D1 /* MD5.swift in Sources */,
 				DDD163142C4C68D300CD525A /* OverrideProvider.swift in Sources */,
 				DDD163142C4C68D300CD525A /* OverrideProvider.swift in Sources */,
@@ -3430,6 +3496,7 @@
 				72F1BD388F42FCA6C52E4500 /* ConfigEditorProvider.swift in Sources */,
 				72F1BD388F42FCA6C52E4500 /* ConfigEditorProvider.swift in Sources */,
 				E39E418C56A5A46B61D960EE /* ConfigEditorStateModel.swift in Sources */,
 				E39E418C56A5A46B61D960EE /* ConfigEditorStateModel.swift in Sources */,
 				45717281F743594AA9D87191 /* ConfigEditorRootView.swift in Sources */,
 				45717281F743594AA9D87191 /* ConfigEditorRootView.swift in Sources */,
+				DD32CFA02CC824D6003686D6 /* TrioRemoteControl+APNS.swift in Sources */,
 				CE7CA3532A064973004BE681 /* tempPresetIntent.swift in Sources */,
 				CE7CA3532A064973004BE681 /* tempPresetIntent.swift in Sources */,
 				D6DEC113821A7F1056C4AA1E /* NightscoutConfigDataFlow.swift in Sources */,
 				D6DEC113821A7F1056C4AA1E /* NightscoutConfigDataFlow.swift in Sources */,
 				DD6B7CBB2C7FBBFA00B75029 /* ReviewInsulinActionView.swift in Sources */,
 				DD6B7CBB2C7FBBFA00B75029 /* ReviewInsulinActionView.swift in Sources */,
@@ -3480,6 +3547,7 @@
 				CE7CA3512A064973004BE681 /* ApplyTempPresetIntent.swift in Sources */,
 				CE7CA3512A064973004BE681 /* ApplyTempPresetIntent.swift in Sources */,
 				FA630397F76B582C8D8681A7 /* BasalProfileEditorProvider.swift in Sources */,
 				FA630397F76B582C8D8681A7 /* BasalProfileEditorProvider.swift in Sources */,
 				DD1745172C54389F00211FAC /* FeatureSettingsView.swift in Sources */,
 				DD1745172C54389F00211FAC /* FeatureSettingsView.swift in Sources */,
+				DD9ECB712CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift in Sources */,
 				63E890B4D951EAA91C071D5C /* BasalProfileEditorStateModel.swift in Sources */,
 				63E890B4D951EAA91C071D5C /* BasalProfileEditorStateModel.swift in Sources */,
 				38FEF3FA2737E42000574A46 /* BaseStateModel.swift in Sources */,
 				38FEF3FA2737E42000574A46 /* BaseStateModel.swift in Sources */,
 				CC6C406E2ACDD69E009B8058 /* RawFetchedProfile.swift in Sources */,
 				CC6C406E2ACDD69E009B8058 /* RawFetchedProfile.swift in Sources */,
@@ -3508,6 +3576,7 @@
 				38192E0D261BAF980094D973 /* ConvenienceExtensions.swift in Sources */,
 				38192E0D261BAF980094D973 /* ConvenienceExtensions.swift in Sources */,
 				88AB39B23C9552BD6E0C9461 /* ISFEditorRootView.swift in Sources */,
 				88AB39B23C9552BD6E0C9461 /* ISFEditorRootView.swift in Sources */,
 				BD6EB2D62C7D049B0086BBB6 /* LiveActivityWidgetConfiguration.swift in Sources */,
 				BD6EB2D62C7D049B0086BBB6 /* LiveActivityWidgetConfiguration.swift in Sources */,
+				DD32CF982CC82463003686D6 /* TrioRemoteControl+Bolus.swift in Sources */,
 				F816825E28DB441200054060 /* HeartBeatManager.swift in Sources */,
 				F816825E28DB441200054060 /* HeartBeatManager.swift in Sources */,
 				58F107742BD1A4D000B1A680 /* Determination+helper.swift in Sources */,
 				58F107742BD1A4D000B1A680 /* Determination+helper.swift in Sources */,
 				38FEF413273B317A00574A46 /* HKUnit.swift in Sources */,
 				38FEF413273B317A00574A46 /* HKUnit.swift in Sources */,
@@ -3551,9 +3620,11 @@
 				38A00B1F25FC00F7006BC0B0 /* Autotune.swift in Sources */,
 				38A00B1F25FC00F7006BC0B0 /* Autotune.swift in Sources */,
 				38AAF85525FFF846004AF583 /* CurrentGlucoseView.swift in Sources */,
 				38AAF85525FFF846004AF583 /* CurrentGlucoseView.swift in Sources */,
 				041D1E995A6AE92E9289DC49 /* BolusDataFlow.swift in Sources */,
 				041D1E995A6AE92E9289DC49 /* BolusDataFlow.swift in Sources */,
+				DD32CF9E2CC824C5003686D6 /* TrioRemoteControl+Override.swift in Sources */,
 				23888883D4EA091C88480FF2 /* BolusProvider.swift in Sources */,
 				23888883D4EA091C88480FF2 /* BolusProvider.swift in Sources */,
 				38E98A2D25F52DC400C0CED0 /* NSLocking+Extensions.swift in Sources */,
 				38E98A2D25F52DC400C0CED0 /* NSLocking+Extensions.swift in Sources */,
 				BDBAACFA2C2D439700370AAE /* OverrideData.swift in Sources */,
 				BDBAACFA2C2D439700370AAE /* OverrideData.swift in Sources */,
+				DD9ECB682CA99F4500AA7C45 /* TrioRemoteControl.swift in Sources */,
 				38569353270B5E350002C50D /* CGMRootView.swift in Sources */,
 				38569353270B5E350002C50D /* CGMRootView.swift in Sources */,
 				69A31254F2451C20361D172F /* BolusStateModel.swift in Sources */,
 				69A31254F2451C20361D172F /* BolusStateModel.swift in Sources */,
 				1967DFC029D053AC00759F30 /* IconSelection.swift in Sources */,
 				1967DFC029D053AC00759F30 /* IconSelection.swift in Sources */,
@@ -3641,6 +3712,7 @@
 				DDE1796E2C910127003CDDB7 /* OrefDetermination+CoreDataClass.swift in Sources */,
 				DDE1796E2C910127003CDDB7 /* OrefDetermination+CoreDataClass.swift in Sources */,
 				DDE1796F2C910127003CDDB7 /* OrefDetermination+CoreDataProperties.swift in Sources */,
 				DDE1796F2C910127003CDDB7 /* OrefDetermination+CoreDataProperties.swift in Sources */,
 				DDE179702C910127003CDDB7 /* OverrideStored+CoreDataClass.swift in Sources */,
 				DDE179702C910127003CDDB7 /* OverrideStored+CoreDataClass.swift in Sources */,
+				DD9ECB742CA9A0C300AA7C45 /* RemoteControlConfig.swift in Sources */,
 				DDE179712C910127003CDDB7 /* OverrideStored+CoreDataProperties.swift in Sources */,
 				DDE179712C910127003CDDB7 /* OverrideStored+CoreDataProperties.swift in Sources */,
 				CD78BB94E43B249D60CC1A1B /* GlucoseNotificationSettingsRootView.swift in Sources */,
 				CD78BB94E43B249D60CC1A1B /* GlucoseNotificationSettingsRootView.swift in Sources */,
 				CE7CA3502A064973004BE681 /* CancelTempPresetIntent.swift in Sources */,
 				CE7CA3502A064973004BE681 /* CancelTempPresetIntent.swift in Sources */,

+ 2 - 0
FreeAPS/Resources/FreeAPS.entitlements

@@ -2,6 +2,8 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <plist version="1.0">
 <dict>
 <dict>
+	<key>aps-environment</key>
+	<string>development</string>
 	<key>com.apple.developer.healthkit</key>
 	<key>com.apple.developer.healthkit</key>
 	<true/>
 	<true/>
 	<key>com.apple.developer.healthkit.access</key>
 	<key>com.apple.developer.healthkit.access</key>

+ 2 - 0
FreeAPS/Resources/Info.plist

@@ -2,6 +2,8 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <plist version="1.0">
 <dict>
 <dict>
+	<key>TeamID</key>
+	<string>$(DEVELOPER_TEAM)</string>
 	<key>AppGroupID</key>
 	<key>AppGroupID</key>
 	<string>$(APP_GROUP_ID)</string>
 	<string>$(APP_GROUP_ID)</string>
 	<key>BGTaskSchedulerPermittedIdentifiers</key>
 	<key>BGTaskSchedulerPermittedIdentifiers</key>

+ 11 - 1
FreeAPS/Sources/APS/FetchGlucoseManager.swift

@@ -1,5 +1,6 @@
 import Combine
 import Combine
 import Foundation
 import Foundation
+import HealthKit
 import LoopKit
 import LoopKit
 import LoopKitUI
 import LoopKitUI
 import SwiftDate
 import SwiftDate
@@ -101,6 +102,14 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         settingsManager.settings.uploadGlucose = cgmM.shouldSyncToRemoteService
         settingsManager.settings.uploadGlucose = cgmM.shouldSyncToRemoteService
     }
     }
 
 
+    private func updateManagerUnits(_ manager: CGMManagerUI?) {
+        let units = settingsManager.settings.units
+        let managerName = cgmManager.map { "\(type(of: $0))" } ?? "nil"
+        let loopkitUnits: HKUnit = units == .mgdL ? .milligramsPerDeciliter : .millimolesPerLiter
+        print("manager: \(managerName) is changing units to: \(loopkitUnits.description) ")
+        manager?.unitDidChange(to: loopkitUnits)
+    }
+
     func updateGlucoseSource(cgmGlucoseSourceType: CGMType, cgmGlucosePluginId: String, newManager: CGMManagerUI?) {
     func updateGlucoseSource(cgmGlucoseSourceType: CGMType, cgmGlucosePluginId: String, newManager: CGMManagerUI?) {
         // if changed, remove all calibrations
         // if changed, remove all calibrations
         if self.cgmGlucoseSourceType != cgmGlucoseSourceType || self.cgmGlucosePluginId != cgmGlucosePluginId {
         if self.cgmGlucoseSourceType != cgmGlucoseSourceType || self.cgmGlucosePluginId != cgmGlucosePluginId {
@@ -123,6 +132,8 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
             removeCalibrations()
             removeCalibrations()
         } else if self.cgmGlucoseSourceType == .plugin, cgmManager == nil, let rawCGMManager = rawCGMManager {
         } else if self.cgmGlucoseSourceType == .plugin, cgmManager == nil, let rawCGMManager = rawCGMManager {
             cgmManager = cgmManagerFromRawValue(rawCGMManager)
             cgmManager = cgmManagerFromRawValue(rawCGMManager)
+            updateManagerUnits(cgmManager)
+
         } else {
         } else {
             saveConfigManager()
             saveConfigManager()
         }
         }
@@ -152,7 +163,6 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         else {
         else {
             return nil
             return nil
         }
         }
-
         return Manager.init(rawState: rawState)
         return Manager.init(rawState: rawState)
     }
     }
 
 

+ 1 - 0
FreeAPS/Sources/APS/OpenAPS/Constants.swift

@@ -87,6 +87,7 @@ extension OpenAPS {
         static let uploadedPreferences = "upload/uploaded-preferences.json"
         static let uploadedPreferences = "upload/uploaded-preferences.json"
         static let uploadedSettings = "upload/uploaded-settings.json"
         static let uploadedSettings = "upload/uploaded-settings.json"
         static let uploadedManualGlucose = "upload/uploaded-manual-readings.json"
         static let uploadedManualGlucose = "upload/uploaded-manual-readings.json"
+        static let uploadedNotes = "upload/uploaded-notes.json"
     }
     }
 
 
     enum FreeAPS {
     enum FreeAPS {

+ 76 - 7
FreeAPS/Sources/APS/OpenAPS/JavaScriptWorker.swift

@@ -1,6 +1,28 @@
 import Foundation
 import Foundation
 import JavaScriptCore
 import JavaScriptCore
 
 
+private let contextLock = NSRecursiveLock()
+
+extension String {
+    var lowercasingFirst: String { prefix(1).lowercased() + dropFirst() }
+    var uppercasingFirst: String { prefix(1).uppercased() + dropFirst() }
+    var camelCased: String {
+        guard !isEmpty else { return "" }
+        let parts = components(separatedBy: .alphanumerics.inverted)
+        let first = parts.first!.lowercasingFirst
+        let rest = parts.dropFirst().map(\.uppercasingFirst)
+        return ([first] + rest).joined()
+    }
+
+    var pascalCased: String {
+        guard !isEmpty else { return "" }
+        let parts = components(separatedBy: .alphanumerics.inverted)
+        let first = parts.first!.uppercasingFirst
+        let rest = parts.dropFirst().map(\.uppercasingFirst)
+        return ([first] + rest).joined()
+    }
+}
+
 final class JavaScriptWorker {
 final class JavaScriptWorker {
     private let processQueue = DispatchQueue(label: "DispatchQueue.JavaScriptWorker", attributes: .concurrent)
     private let processQueue = DispatchQueue(label: "DispatchQueue.JavaScriptWorker", attributes: .concurrent)
     private let virtualMachine: JSVirtualMachine
     private let virtualMachine: JSVirtualMachine
@@ -22,8 +44,16 @@ final class JavaScriptWorker {
                 warning(.openAPS, "JavaScript Error: \(error)")
                 warning(.openAPS, "JavaScript Error: \(error)")
             }
             }
         }
         }
-        let consoleLog: @convention(block) (String) -> Void = { message in
-            debug(.openAPS, "JavaScript log: \(message)")
+        let consoleLog: @convention(block) (String) -> Void = { [weak context] message in
+            guard let context = context else { return }
+            let trimmedMessage = message.trimmingCharacters(in: .whitespacesAndNewlines)
+            if !trimmedMessage.isEmpty {
+                let fileName = context.objectForKeyedSubscript("scriptName").toString() ?? "Unknown"
+                let threadSafeLog = "\(trimmedMessage)"
+                self.processQueue.async(flags: .barrier) {
+                    self.outputLogs(for: fileName, message: threadSafeLog)
+                }
+            }
         }
         }
         context.setObject(consoleLog, forKeyedSubscript: "_consoleLog" as NSString)
         context.setObject(consoleLog, forKeyedSubscript: "_consoleLog" as NSString)
         return context
         return context
@@ -42,8 +72,41 @@ final class JavaScriptWorker {
         contextPoolLock.unlock()
         contextPoolLock.unlock()
     }
     }
 
 
+    private func outputLogs(for fileName: String, message: String) {
+        let logs = message.trimmingCharacters(in: .whitespacesAndNewlines)
+
+        if logs.isEmpty { return }
+
+        if fileName == "autosens.js" {
+            let sanitizedLogs = logs.split(separator: "\n").map { logLine in
+                logLine.replacingOccurrences(
+                    of: "^[-+=x!]|u\\(|\\)|\\d{1,2}h$",
+                    with: "",
+                    options: .regularExpression
+                )
+            }.joined(separator: "\n")
+
+            sanitizedLogs.split(separator: "\n").forEach { logLine in
+                if !logLine.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
+                    debug(.openAPS, "\(fileName): \(logLine)")
+                }
+            }
+        } else {
+            logs.split(separator: "\n").forEach { logLine in
+                if !logLine.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
+                    debug(.openAPS, "\(fileName): \(logLine)")
+                }
+            }
+        }
+    }
+
     @discardableResult func evaluate(script: Script) -> JSValue! {
     @discardableResult func evaluate(script: Script) -> JSValue! {
-        evaluate(string: script.body)
+        let context = getContext()
+        defer { returnContext(context) }
+        let fileName = URL(fileURLWithPath: script.name).lastPathComponent
+        context.setObject(fileName, forKeyedSubscript: "scriptName" as NSString)
+        let result = context.evaluateScript(script.body)
+        return result
     }
     }
 
 
     private func evaluate(string: String) -> JSValue! {
     private func evaluate(string: String) -> JSValue! {
@@ -63,15 +126,21 @@ final class JavaScriptWorker {
 
 
     func inCommonContext<Value>(execute: (JavaScriptWorker) -> Value) -> Value {
     func inCommonContext<Value>(execute: (JavaScriptWorker) -> Value) -> Value {
         let context = getContext()
         let context = getContext()
-        defer { returnContext(context) }
+        defer {
+            returnContext(context)
+        }
         return execute(self)
         return execute(self)
     }
     }
 
 
     func evaluateBatch(scripts: [Script]) {
     func evaluateBatch(scripts: [Script]) {
-        let ctx = getContext()
-        defer { returnContext(ctx) } // Ensure the context is returned to the pool
+        let context = getContext()
+        defer {
+            returnContext(context)
+        }
         scripts.forEach { script in
         scripts.forEach { script in
-            ctx.evaluateScript(script.body)
+            let fileName = URL(fileURLWithPath: script.name).lastPathComponent
+            context.setObject(fileName, forKeyedSubscript: "scriptName" as NSString)
+            context.evaluateScript(script.body)
         }
         }
     }
     }
 }
 }

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

@@ -971,11 +971,11 @@ final class OpenAPS {
 
 
     private func middlewareScript(name: String) -> Script? {
     private func middlewareScript(name: String) -> Script? {
         if let body = storage.retrieveRaw(name) {
         if let body = storage.retrieveRaw(name) {
-            return Script(name: "Middleware", body: body)
+            return Script(name: name, body: body)
         }
         }
 
 
         if let url = Foundation.Bundle.main.url(forResource: "javascript/\(name)", withExtension: "") {
         if let url = Foundation.Bundle.main.url(forResource: "javascript/\(name)", withExtension: "") {
-            return Script(name: "Middleware", body: try! String(contentsOf: url))
+            return Script(name: name, body: try! String(contentsOf: url))
         }
         }
 
 
         return nil
         return nil

+ 28 - 0
FreeAPS/Sources/APS/Storage/OverrideStorage.swift

@@ -12,6 +12,7 @@ protocol OverrideStorage {
     func deleteOverridePreset(_ objectID: NSManagedObjectID) async
     func deleteOverridePreset(_ objectID: NSManagedObjectID) async
     func getOverridesNotYetUploadedToNightscout() async -> [NightscoutExercise]
     func getOverridesNotYetUploadedToNightscout() async -> [NightscoutExercise]
     func getOverrideRunsNotYetUploadedToNightscout() async -> [NightscoutExercise]
     func getOverrideRunsNotYetUploadedToNightscout() async -> [NightscoutExercise]
+    func getPresetOverridesForNightscout() async -> [NightscoutPresetOverride]
 }
 }
 
 
 final class BaseOverrideStorage: OverrideStorage, Injectable {
 final class BaseOverrideStorage: OverrideStorage, Injectable {
@@ -267,4 +268,31 @@ final class BaseOverrideStorage: OverrideStorage, Injectable {
             }
             }
         }
         }
     }
     }
+
+    func getPresetOverridesForNightscout() async -> [NightscoutPresetOverride] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: OverrideStored.self,
+            onContext: backgroundContext,
+            predicate: NSPredicate.allOverridePresets,
+            key: "orderPosition",
+            ascending: true
+        )
+
+        return await backgroundContext.perform {
+            guard let fetchedResults = results as? [OverrideStored] else { return [] }
+
+            return fetchedResults.map { overrideStored in
+                let duration = overrideStored.duration as? Decimal != 0 ? overrideStored.duration as? Decimal : nil
+                let percentage = overrideStored.percentage != 0 ? overrideStored.percentage : nil
+                let target = (overrideStored.target as? Decimal) != 0 ? overrideStored.target as? Decimal : nil
+
+                return NightscoutPresetOverride(
+                    name: overrideStored.name ?? "",
+                    duration: duration,
+                    percentage: percentage,
+                    target: target
+                )
+            }
+        }
+    }
 }
 }

+ 51 - 1
FreeAPS/Sources/Application/AppDelegate.swift

@@ -1,4 +1,54 @@
 import SwiftUI
 import SwiftUI
 import UIKit
 import UIKit
+import UserNotifications
 
 
-class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject {}
+class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject, UNUserNotificationCenterDelegate {
+    func application(
+        _ application: UIApplication,
+        didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?
+    ) -> Bool {
+        UNUserNotificationCenter.current().delegate = self
+        application.registerForRemoteNotifications()
+        return true
+    }
+
+    func application(
+        _: UIApplication,
+        didReceiveRemoteNotification userInfo: [AnyHashable: Any],
+        fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
+    ) {
+        debug(.remoteControl, "Received notification")
+
+        do {
+            let jsonData = try JSONSerialization.data(withJSONObject: userInfo)
+            let pushMessage = try JSONDecoder().decode(PushMessage.self, from: jsonData)
+
+            Task {
+                await TrioRemoteControl.shared.handleRemoteNotification(pushMessage: pushMessage)
+                completionHandler(.newData)
+            }
+        } catch {
+            debug(.remoteControl, "Error decoding push message: \(error.localizedDescription)")
+            completionHandler(.failed)
+        }
+    }
+
+    func application(
+        _: UIApplication,
+        didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
+    ) {
+        let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) }
+        let token = tokenParts.joined()
+
+        Task {
+            await TrioRemoteControl.shared.handleAPNSChanges(deviceToken: token)
+        }
+    }
+
+    func application(
+        _: UIApplication,
+        didFailToRegisterForRemoteNotificationsWithError error: Error
+    ) {
+        debug(.remoteControl, "Failed to register for remote notifications: \(error.localizedDescription)")
+    }
+}

+ 4 - 0
FreeAPS/Sources/Logger/Logger.swift

@@ -112,6 +112,7 @@ final class Logger {
     static let deviceManager = Logger(category: .deviceManager, reporter: baseReporter)
     static let deviceManager = Logger(category: .deviceManager, reporter: baseReporter)
     static let apsManager = Logger(category: .apsManager, reporter: baseReporter)
     static let apsManager = Logger(category: .apsManager, reporter: baseReporter)
     static let nightscout = Logger(category: .nightscout, reporter: baseReporter)
     static let nightscout = Logger(category: .nightscout, reporter: baseReporter)
+    static let remoteControl = Logger(category: .remoteControl, reporter: baseReporter)
 
 
     enum Category: String {
     enum Category: String {
         case `default`
         case `default`
@@ -121,6 +122,7 @@ final class Logger {
         case deviceManager
         case deviceManager
         case apsManager
         case apsManager
         case nightscout
         case nightscout
+        case remoteControl
 
 
         var name: String {
         var name: String {
             rawValue.capitalizingFirstLetter()
             rawValue.capitalizingFirstLetter()
@@ -135,6 +137,7 @@ final class Logger {
             case .deviceManager: return .deviceManager
             case .deviceManager: return .deviceManager
             case .apsManager: return .apsManager
             case .apsManager: return .apsManager
             case .nightscout: return .nightscout
             case .nightscout: return .nightscout
+            case .remoteControl: return .remoteControl
             }
             }
         }
         }
 
 
@@ -147,6 +150,7 @@ final class Logger {
                  .deviceManager,
                  .deviceManager,
                  .nightscout,
                  .nightscout,
                  .openAPS,
                  .openAPS,
+                 .remoteControl,
                  .service:
                  .service:
                 return OSLog(subsystem: subsystem, category: name)
                 return OSLog(subsystem: subsystem, category: name)
             }
             }

+ 12 - 0
FreeAPS/Sources/Models/NightscoutStatus.swift

@@ -52,4 +52,16 @@ struct NightscoutProfileStore: JSON {
     let units: String
     let units: String
     let enteredBy: String
     let enteredBy: String
     let store: [String: ScheduledNightscoutProfile]
     let store: [String: ScheduledNightscoutProfile]
+    let bundleIdentifier: String
+    let deviceToken: String
+    let isAPNSProduction: Bool
+    let overridePresets: [NightscoutPresetOverride]?
+    let teamID: String
+}
+
+struct NightscoutPresetOverride: JSON {
+    let name: String
+    let duration: Decimal?
+    let percentage: Double?
+    let target: Decimal?
 }
 }

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

@@ -77,6 +77,7 @@ enum EventType: String, JSON {
     case nsAnnouncement = "Announcement"
     case nsAnnouncement = "Announcement"
     case nsSensorChange = "Sensor Start"
     case nsSensorChange = "Sensor Start"
     case capillaryGlucose = "BG Check"
     case capillaryGlucose = "BG Check"
+    case note = "Note"
 }
 }
 
 
 enum TempType: String, JSON {
 enum TempType: String, JSON {

+ 141 - 0
FreeAPS/Sources/Models/PushMessage.swift

@@ -0,0 +1,141 @@
+import Foundation
+
+struct PushMessage: Codable, Sendable {
+    var user: String
+    var commandType: TrioRemoteControl.CommandType
+    var bolusAmount: Decimal?
+    var target: Int?
+    var duration: Int?
+    var carbs: Int?
+    var protein: Int?
+    var fat: Int?
+    var sharedSecret: String
+    var timestamp: TimeInterval
+    var overrideName: String?
+    var scheduledTime: TimeInterval?
+
+    enum CodingKeys: String, CodingKey {
+        case aps
+        case user
+        case commandType = "command_type"
+        case bolusAmount = "bolus_amount"
+        case target
+        case duration
+        case carbs
+        case protein
+        case fat
+        case sharedSecret = "shared_secret"
+        case timestamp
+        case overrideName
+        case scheduledTime = "scheduled_time"
+    }
+
+    func encode(to encoder: Encoder) throws {
+        var container = encoder.container(keyedBy: CodingKeys.self)
+        try container.encode(user, forKey: .user)
+        try container.encode(commandType, forKey: .commandType)
+        try container.encodeIfPresent(bolusAmount, forKey: .bolusAmount)
+        try container.encodeIfPresent(target, forKey: .target)
+        try container.encodeIfPresent(duration, forKey: .duration)
+        try container.encodeIfPresent(carbs, forKey: .carbs)
+        try container.encodeIfPresent(protein, forKey: .protein)
+        try container.encodeIfPresent(fat, forKey: .fat)
+        try container.encode(sharedSecret, forKey: .sharedSecret)
+        try container.encode(timestamp, forKey: .timestamp)
+        try container.encodeIfPresent(overrideName, forKey: .overrideName)
+        if let scheduledTime = scheduledTime {
+            try container.encode(scheduledTime, forKey: .scheduledTime)
+        }
+    }
+
+    init(from decoder: Decoder) throws {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+        user = try container.decode(String.self, forKey: .user)
+        commandType = try container.decode(TrioRemoteControl.CommandType.self, forKey: .commandType)
+        bolusAmount = try container.decodeIfPresent(Decimal.self, forKey: .bolusAmount)
+        target = try container.decodeIfPresent(Int.self, forKey: .target)
+        duration = try container.decodeIfPresent(Int.self, forKey: .duration)
+        carbs = try container.decodeIfPresent(Int.self, forKey: .carbs)
+        protein = try container.decodeIfPresent(Int.self, forKey: .protein)
+        fat = try container.decodeIfPresent(Int.self, forKey: .fat)
+        sharedSecret = try container.decode(String.self, forKey: .sharedSecret)
+        timestamp = try container.decode(TimeInterval.self, forKey: .timestamp)
+        overrideName = try container.decodeIfPresent(String.self, forKey: .overrideName)
+        scheduledTime = try container.decodeIfPresent(TimeInterval.self, forKey: .scheduledTime)
+    }
+
+    init(
+        user: String,
+        commandType: TrioRemoteControl.CommandType,
+        bolusAmount: Decimal? = nil,
+        target: Int? = nil,
+        duration: Int? = nil,
+        carbs: Int? = nil,
+        protein: Int? = nil,
+        fat: Int? = nil,
+        sharedSecret: String,
+        timestamp: TimeInterval,
+        overrideName: String? = nil,
+        scheduledTime: TimeInterval? = nil
+    ) {
+        self.user = user
+        self.commandType = commandType
+        self.bolusAmount = bolusAmount
+        self.target = target
+        self.duration = duration
+        self.carbs = carbs
+        self.protein = protein
+        self.fat = fat
+        self.sharedSecret = sharedSecret
+        self.timestamp = timestamp
+        self.overrideName = overrideName
+        self.scheduledTime = scheduledTime
+    }
+
+    func humanReadableDescription() -> String {
+        var description = "User: \(user). Command Type: \(commandType.description). "
+
+        if let override = overrideName {
+            description += "Override Name: \(override). "
+        }
+
+        switch commandType {
+        case .bolus:
+            if let amount = bolusAmount {
+                description += "Bolus Amount: \(amount) units."
+            } else {
+                description += "Bolus Amount: unknown."
+            }
+        case .tempTarget:
+            let targetDesc = target != nil ? "\(target!) mg/dL" : "unknown target"
+            let durationDesc = duration != nil ? "\(duration!) minutes" : "unknown duration"
+            description += "Temp Target: \(targetDesc), Duration: \(durationDesc)."
+        case .cancelTempTarget:
+            description += "Cancel Temp Target command."
+        case .meal:
+            let carbsDesc = carbs != nil ? "\(carbs!)g carbs" : "unknown carbs"
+            let fatDesc = fat != nil ? "\(fat!)g fat" : "unknown fat"
+            let proteinDesc = protein != nil ? "\(protein!)g protein" : "unknown protein"
+            description += "Meal with \(carbsDesc), \(fatDesc), \(proteinDesc)."
+        case .startOverride:
+            if let override = overrideName {
+                description += "Start Override: \(override)."
+            } else {
+                description += "Start Override: unknown override name."
+            }
+        case .cancelOverride:
+            description += "Cancel Override command."
+        }
+
+        if let scheduledTime = scheduledTime {
+            let date = Date(timeIntervalSince1970: scheduledTime)
+            let formatter = DateFormatter()
+            formatter.dateStyle = .short
+            formatter.timeStyle = .short
+            let dateString = formatter.string(from: date)
+            description += " Scheduled for: \(dateString)."
+        }
+
+        return description
+    }
+}

+ 7 - 7
FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift

@@ -104,11 +104,11 @@ extension Bolus {
         var preprocessedData: [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)] = []
         var preprocessedData: [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)] = []
         var predictionsForChart: Predictions?
         var predictionsForChart: Predictions?
         var simulatedDetermination: Determination?
         var simulatedDetermination: Determination?
-        var determinationObjectIDs: [NSManagedObjectID] = []
+        @MainActor var determinationObjectIDs: [NSManagedObjectID] = []
 
 
         var minForecast: [Int] = []
         var minForecast: [Int] = []
         var maxForecast: [Int] = []
         var maxForecast: [Int] = []
-        var minCount: Int = 12 // count of Forecasts drawn in 5 min distances, i.e. 12 means a min of 1 hour
+        @MainActor var minCount: Int = 12 // count of Forecasts drawn in 5 min distances, i.e. 12 means a min of 1 hour
         var forecastDisplayType: ForecastDisplayType = .cone
         var forecastDisplayType: ForecastDisplayType = .cone
         var isSmoothingEnabled: Bool = false
         var isSmoothingEnabled: Bool = false
         var stops: [Gradient.Stop] = []
         var stops: [Gradient.Stop] = []
@@ -736,7 +736,7 @@ extension Bolus.StateModel {
         if let forecastData = forecastData {
         if let forecastData = forecastData {
             simulatedDetermination = forecastData
             simulatedDetermination = forecastData
         } else {
         } else {
-            simulatedDetermination = await Task.detached { [self] in
+            simulatedDetermination = await Task { [self] in
                 await apsManager.simulateDetermineBasal(carbs: carbs, iob: amount)
                 await apsManager.simulateDetermineBasal(carbs: carbs, iob: amount)
             }.value
             }.value
         }
         }
@@ -761,14 +761,14 @@ extension Bolus.StateModel {
         minCount = max(12, nonEmptyArrays.map(\.count).min() ?? 0)
         minCount = max(12, nonEmptyArrays.map(\.count).min() ?? 0)
         guard minCount > 0 else { return }
         guard minCount > 0 else { return }
 
 
-        async let minForecastResult = Task.detached {
-            (0 ..< self.minCount).map { index in
+        async let minForecastResult = Task {
+            await (0 ..< self.minCount).map { index in
                 nonEmptyArrays.compactMap { $0.indices.contains(index) ? $0[index] : nil }.min() ?? 0
                 nonEmptyArrays.compactMap { $0.indices.contains(index) ? $0[index] : nil }.min() ?? 0
             }
             }
         }.value
         }.value
 
 
-        async let maxForecastResult = Task.detached {
-            (0 ..< self.minCount).map { index in
+        async let maxForecastResult = Task {
+            await (0 ..< self.minCount).map { index in
                 nonEmptyArrays.compactMap { $0.indices.contains(index) ? $0[index] : nil }.max() ?? 0
                 nonEmptyArrays.compactMap { $0.indices.contains(index) ? $0[index] : nil }.max() ?? 0
             }
             }
         }.value
         }.value

+ 16 - 0
FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift

@@ -67,6 +67,22 @@ extension NightscoutConfig {
             importedInsulinActionCurve = pumpSettings.insulinActionCurve
             importedInsulinActionCurve = pumpSettings.insulinActionCurve
 
 
             isConnectedToNS = nightscoutAPI != nil
             isConnectedToNS = nightscoutAPI != nil
+
+            $isUploadEnabled
+                .dropFirst()
+                .removeDuplicates()
+                .sink { [weak self] enabled in
+                    guard let self = self else { return }
+                    if enabled {
+                        debug(.nightscout, "Upload has been enabled by the user.")
+                        Task {
+                            await self.nightscoutManager.uploadProfiles()
+                        }
+                    } else {
+                        debug(.nightscout, "Upload has been disabled by the user.")
+                    }
+                }
+                .store(in: &lifetime)
         }
         }
 
 
         func connect() {
         func connect() {

+ 9 - 0
FreeAPS/Sources/Modules/OverrideConfig/OverrideStateModel.swift

@@ -9,6 +9,7 @@ extension OverrideConfig {
         @ObservationIgnored @Injected() var storage: TempTargetsStorage!
         @ObservationIgnored @Injected() var storage: TempTargetsStorage!
         @ObservationIgnored @Injected() var apsManager: APSManager!
         @ObservationIgnored @Injected() var apsManager: APSManager!
         @ObservationIgnored @Injected() var overrideStorage: OverrideStorage!
         @ObservationIgnored @Injected() var overrideStorage: OverrideStorage!
+        @ObservationIgnored @Injected() var nightscoutManager: NightscoutManager!
 
 
         var overrideSliderPercentage: Double = 100
         var overrideSliderPercentage: Double = 100
         var isEnabled = false
         var isEnabled = false
@@ -123,6 +124,10 @@ extension OverrideConfig.StateModel {
 
 
             // Update Presets View
             // Update Presets View
             setupOverridePresetsArray()
             setupOverridePresetsArray()
+
+            Task {
+                await nightscoutManager.uploadProfiles()
+            }
         } catch {
         } catch {
             debugPrint(
             debugPrint(
                 "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save after reordering Override Presets with error: \(error.localizedDescription)"
                 "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save after reordering Override Presets with error: \(error.localizedDescription)"
@@ -286,6 +291,8 @@ extension OverrideConfig.StateModel {
 
 
         // Update Presets View
         // Update Presets View
         setupOverridePresetsArray()
         setupOverridePresetsArray()
+
+        await nightscoutManager.uploadProfiles()
     }
     }
 
 
     // MARK: - Setup Override Presets Array
     // MARK: - Setup Override Presets Array
@@ -318,6 +325,8 @@ extension OverrideConfig.StateModel {
 
 
         // Update Presets View
         // Update Presets View
         setupOverridePresetsArray()
         setupOverridePresetsArray()
+
+        await nightscoutManager.uploadProfiles()
     }
     }
 
 
     // MARK: - Setup the State variables with the last Override configuration
     // MARK: - Setup the State variables with the last Override configuration

+ 1 - 1
FreeAPS/Sources/Modules/OverrideConfig/View/AddOverrideForm.swift

@@ -3,7 +3,7 @@ import SwiftUI
 
 
 struct AddOverrideForm: View {
 struct AddOverrideForm: View {
     @Environment(\.presentationMode) var presentationMode
     @Environment(\.presentationMode) var presentationMode
-    @StateObject var state: OverrideConfig.StateModel
+    @Bindable var state: OverrideConfig.StateModel
     @State private var isEditing = false
     @State private var isEditing = false
     @State private var overrideTarget = false
     @State private var overrideTarget = false
     @Environment(\.colorScheme) var colorScheme
     @Environment(\.colorScheme) var colorScheme

+ 4 - 2
FreeAPS/Sources/Modules/OverrideConfig/View/EditOverrideForm.swift

@@ -2,7 +2,7 @@ import Foundation
 import SwiftUI
 import SwiftUI
 
 
 struct EditOverrideForm: View {
 struct EditOverrideForm: View {
-    @ObservedObject var override: OverrideStored
+    var override: OverrideStored
     @Environment(\.presentationMode) var presentationMode
     @Environment(\.presentationMode) var presentationMode
     @Environment(\.colorScheme) var colorScheme
     @Environment(\.colorScheme) var colorScheme
     @Bindable var state: OverrideConfig.StateModel
     @Bindable var state: OverrideConfig.StateModel
@@ -294,7 +294,9 @@ struct EditOverrideForm: View {
                         guard let moc = override.managedObjectContext else { return }
                         guard let moc = override.managedObjectContext else { return }
                         guard moc.hasChanges else { return }
                         guard moc.hasChanges else { return }
                         try moc.save()
                         try moc.save()
-
+                        Task {
+                            await state.nightscoutManager.uploadProfiles()
+                        }
                         if let currentActiveOverride = state.currentActiveOverride {
                         if let currentActiveOverride = state.currentActiveOverride {
                             Task {
                             Task {
                                 await state.disableAllActiveOverrides(
                                 await state.disableAllActiveOverrides(

+ 36 - 0
FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+APNS.swift

@@ -0,0 +1,36 @@
+import Foundation
+
+extension TrioRemoteControl {
+    internal func handleAPNSChanges(deviceToken: String?) async {
+        let previousDeviceToken = UserDefaults.standard.string(forKey: "deviceToken")
+        let previousIsAPNSProduction = UserDefaults.standard.bool(forKey: "isAPNSProduction")
+
+        let isAPNSProduction = isRunningInAPNSProductionEnvironment()
+        var shouldUploadProfiles = false
+
+        if let token = deviceToken, token != previousDeviceToken {
+            UserDefaults.standard.set(token, forKey: "deviceToken")
+            debug(.remoteControl, "Device token updated: \(token)")
+            shouldUploadProfiles = true
+        }
+
+        if previousIsAPNSProduction != isAPNSProduction {
+            UserDefaults.standard.set(isAPNSProduction, forKey: "isAPNSProduction")
+            debug(.remoteControl, "APNS environment changed to: \(isAPNSProduction ? "Production" : "Sandbox")")
+            shouldUploadProfiles = true
+        }
+
+        if shouldUploadProfiles {
+            await nightscoutManager.uploadProfiles()
+        } else {
+            debug(.remoteControl, "No changes detected in device token or APNS environment.")
+        }
+    }
+
+    private func isRunningInAPNSProductionEnvironment() -> Bool {
+        if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL {
+            return appStoreReceiptURL.lastPathComponent != "sandboxReceipt"
+        }
+        return false
+    }
+}

+ 108 - 0
FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+Bolus.swift

@@ -0,0 +1,108 @@
+import Foundation
+
+extension TrioRemoteControl {
+    internal func handleBolusCommand(_ pushMessage: PushMessage) async {
+        guard let bolusAmount = pushMessage.bolusAmount else {
+            await logError("Command rejected: bolus amount is missing or invalid.", pushMessage: pushMessage)
+            return
+        }
+
+        let maxBolus = await FreeAPSApp.resolver.resolve(SettingsManager.self)?.pumpSettings.maxBolus ?? Decimal(0)
+
+        if bolusAmount > maxBolus {
+            await logError(
+                "Command rejected: bolus amount (\(bolusAmount) units) exceeds the maximum allowed (\(maxBolus) units).",
+                pushMessage: pushMessage
+            )
+            return
+        }
+
+        let maxIOB = settings.preferences.maxIOB
+        let currentIOB = await fetchCurrentIOB()
+        if (currentIOB + bolusAmount) > maxIOB {
+            await logError(
+                "Command rejected: bolus amount (\(bolusAmount) units) would exceed max IOB (\(maxIOB) units). Current IOB: \(currentIOB) units.",
+                pushMessage: pushMessage
+            )
+            return
+        }
+
+        let totalRecentBolusAmount = await fetchTotalRecentBolusAmount(since: Date(timeIntervalSince1970: pushMessage.timestamp))
+
+        if totalRecentBolusAmount >= bolusAmount * 0.2 {
+            await logError(
+                "Command rejected: boluses totaling more than 20% of the requested amount have been delivered since the command was sent.",
+                pushMessage: pushMessage
+            )
+            return
+        }
+
+        debug(.remoteControl, "Enacting bolus command with amount: \(bolusAmount) units.")
+
+        guard let apsManager = await FreeAPSApp.resolver.resolve(APSManager.self) else {
+            await logError(
+                "Error: unable to process bolus command because the APS Manager is not available.",
+                pushMessage: pushMessage
+            )
+            return
+        }
+
+        await apsManager.enactBolus(amount: Double(truncating: bolusAmount as NSNumber), isSMB: false)
+
+        debug(
+            .remoteControl,
+            "Remote command processed successfully. \(pushMessage.humanReadableDescription())"
+        )
+    }
+
+    private func fetchCurrentIOB() async -> Decimal {
+        let predicate = NSPredicate.predicateFor30MinAgoForDetermination
+
+        let determinations = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: OrefDetermination.self,
+            onContext: pumpHistoryFetchContext,
+            predicate: predicate,
+            key: "timestamp",
+            ascending: false,
+            fetchLimit: 1,
+            propertiesToFetch: ["iob"]
+        )
+
+        guard let fetchedResults = determinations as? [[String: Any]],
+              let firstResult = fetchedResults.first,
+              let iob = firstResult["iob"] as? Decimal
+        else {
+            await logError("Failed to fetch current IOB.")
+            return Decimal(0)
+        }
+
+        return iob
+    }
+
+    private func fetchTotalRecentBolusAmount(since date: Date) async -> Decimal {
+        let predicate = NSPredicate(
+            format: "type == %@ AND timestamp > %@",
+            PumpEventStored.EventType.bolus.rawValue,
+            date as NSDate
+        )
+
+        let results: Any = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: PumpEventStored.self,
+            onContext: pumpHistoryFetchContext,
+            predicate: predicate,
+            key: "timestamp",
+            ascending: true,
+            fetchLimit: nil,
+            propertiesToFetch: ["bolus.amount"]
+        )
+
+        guard let bolusDictionaries = results as? [[String: Any]] else {
+            await logError("Failed to cast fetched bolus events. Fetched entities type: \(type(of: results))")
+            return 0
+        }
+
+        let totalAmount = bolusDictionaries.compactMap { ($0["bolus.amount"] as? NSNumber)?.decimalValue }.reduce(0, +)
+
+        return totalAmount
+    }
+}

+ 12 - 0
FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+Helpers.swift

@@ -0,0 +1,12 @@
+import Foundation
+
+extension TrioRemoteControl {
+    func logError(_ errorMessage: String, pushMessage: PushMessage? = nil) async {
+        var note = errorMessage
+        if let pushMessage = pushMessage {
+            note += " Details: \(pushMessage.humanReadableDescription())"
+        }
+        debug(.remoteControl, note)
+        await nightscoutManager.uploadNoteTreatment(note: note)
+    }
+}

+ 82 - 0
FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+Meal.swift

@@ -0,0 +1,82 @@
+import Foundation
+
+extension TrioRemoteControl {
+    func handleMealCommand(_ pushMessage: PushMessage) async {
+        guard pushMessage.carbs != nil || pushMessage.fat != nil || pushMessage.protein != nil else {
+            await logError("Command rejected: meal data is incomplete or invalid.", pushMessage: pushMessage)
+            return
+        }
+
+        let carbsDecimal = pushMessage.carbs != nil ? Decimal(pushMessage.carbs!) : nil
+        let fatDecimal = pushMessage.fat != nil ? Decimal(pushMessage.fat!) : nil
+        let proteinDecimal = pushMessage.protein != nil ? Decimal(pushMessage.protein!) : nil
+
+        let settings = await FreeAPSApp.resolver.resolve(SettingsManager.self)?.settings
+        let maxCarbs = settings?.maxCarbs ?? Decimal(0)
+        let maxFat = settings?.maxFat ?? Decimal(0)
+        let maxProtein = settings?.maxProtein ?? Decimal(0)
+
+        if let carbs = carbsDecimal, carbs > maxCarbs {
+            await logError(
+                "Command rejected: carbs amount (\(carbs)g) exceeds the maximum allowed (\(maxCarbs)g).",
+                pushMessage: pushMessage
+            )
+            return
+        }
+
+        if let fat = fatDecimal, fat > maxFat {
+            await logError(
+                "Command rejected: fat amount (\(fat)g) exceeds the maximum allowed (\(maxFat)g).",
+                pushMessage: pushMessage
+            )
+            return
+        }
+
+        if let protein = proteinDecimal, protein > maxProtein {
+            await logError(
+                "Command rejected: protein amount (\(protein)g) exceeds the maximum allowed (\(maxProtein)g).",
+                pushMessage: pushMessage
+            )
+            return
+        }
+
+        let pushMessageDate = Date(timeIntervalSince1970: pushMessage.timestamp)
+        let recentCarbEntries = carbsStorage.recent()
+        let carbsAfterPushMessage = recentCarbEntries.filter { $0.createdAt > pushMessageDate }
+
+        if !carbsAfterPushMessage.isEmpty {
+            await logError(
+                "Command rejected: newer carb entries have been logged since the command was sent.",
+                pushMessage: pushMessage
+            )
+            return
+        }
+
+        let actualDate: Date?
+        if let scheduledTime = pushMessage.scheduledTime {
+            actualDate = Date(timeIntervalSince1970: scheduledTime)
+        } else {
+            actualDate = nil
+        }
+
+        let mealEntry = CarbsEntry(
+            id: UUID().uuidString,
+            createdAt: Date(),
+            actualDate: actualDate,
+            carbs: carbsDecimal ?? 0,
+            fat: fatDecimal,
+            protein: proteinDecimal,
+            note: "Remote meal command",
+            enteredBy: CarbsEntry.manual,
+            isFPU: false,
+            fpuID: nil
+        )
+
+        await carbsStorage.storeCarbs([mealEntry], areFetchedFromRemote: false)
+
+        debug(
+            .remoteControl,
+            "Remote command processed successfully. \(pushMessage.humanReadableDescription())"
+        )
+    }
+}

+ 100 - 0
FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+Override.swift

@@ -0,0 +1,100 @@
+import Foundation
+
+extension TrioRemoteControl {
+    @MainActor internal func handleCancelOverrideCommand(_ pushMessage: PushMessage) async {
+        await disableAllActiveOverrides()
+
+        debug(
+            .remoteControl,
+            "Remote command processed successfully. \(pushMessage.humanReadableDescription())"
+        )
+    }
+
+    @MainActor internal func handleStartOverrideCommand(_ pushMessage: PushMessage) async {
+        guard let overrideName = pushMessage.overrideName, !overrideName.isEmpty else {
+            await logError("Command rejected: override name is missing.", pushMessage: pushMessage)
+            return
+        }
+
+        let presetIDs = await overrideStorage.fetchForOverridePresets()
+
+        let presets = presetIDs.compactMap { id in
+            try? viewContext.existingObject(with: id) as? OverrideStored
+        }
+
+        if let preset = presets.first(where: { $0.name == overrideName }) {
+            await enactOverridePreset(preset: preset, pushMessage: pushMessage)
+        } else {
+            await logError("Command rejected: override preset '\(overrideName)' not found.", pushMessage: pushMessage)
+        }
+    }
+
+    @MainActor private func enactOverridePreset(preset: OverrideStored, pushMessage: PushMessage) async {
+        await disableAllActiveOverrides()
+
+        preset.enabled = true
+        preset.date = Date()
+        preset.isUploadedToNS = false
+
+        do {
+            if viewContext.hasChanges {
+                try viewContext.save()
+
+                Foundation.NotificationCenter.default.post(name: .willUpdateOverrideConfiguration, object: nil)
+                await awaitNotification(.didUpdateOverrideConfiguration)
+
+                debug(
+                    .remoteControl,
+                    "Remote command processed successfully. \(pushMessage.humanReadableDescription())"
+                )
+            }
+        } catch {
+            debug(.remoteControl, "Failed to enact override preset: \(error.localizedDescription)")
+        }
+    }
+
+    @MainActor private func disableAllActiveOverrides() async {
+        let ids = await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 0) // 0 = no fetch limit
+
+        let didPostNotification = await viewContext.perform { () -> Bool in
+            do {
+                let results = try ids.compactMap { id in
+                    try self.viewContext.existingObject(with: id) as? OverrideStored
+                }
+
+                guard !results.isEmpty else { return false }
+
+                for canceledOverride in results where canceledOverride.enabled {
+                    let newOverrideRunStored = OverrideRunStored(context: self.viewContext)
+                    newOverrideRunStored.id = UUID()
+                    newOverrideRunStored.name = canceledOverride.name
+                    newOverrideRunStored.startDate = canceledOverride.date ?? .distantPast
+                    newOverrideRunStored.endDate = Date()
+                    newOverrideRunStored
+                        .target = NSDecimalNumber(decimal: self.overrideStorage.calculateTarget(override: canceledOverride))
+                    newOverrideRunStored.override = canceledOverride
+                    newOverrideRunStored.isUploadedToNS = false
+
+                    canceledOverride.enabled = false
+                }
+
+                if self.viewContext.hasChanges {
+                    try self.viewContext.save()
+                    Foundation.NotificationCenter.default.post(name: .willUpdateOverrideConfiguration, object: nil)
+                    return true
+                } else {
+                    return false
+                }
+            } catch {
+                debugPrint(
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to disable active Overrides with error: \(error.localizedDescription)"
+                )
+                return false
+            }
+        }
+
+        if didPostNotification {
+            await awaitNotification(.didUpdateOverrideConfiguration)
+        }
+    }
+}

+ 49 - 0
FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+TempTarget.swift

@@ -0,0 +1,49 @@
+import Foundation
+
+extension TrioRemoteControl {
+    func handleTempTargetCommand(_ pushMessage: PushMessage) async {
+        guard let targetValue = pushMessage.target,
+              let durationValue = pushMessage.duration
+        else {
+            await logError("Command rejected: temp target data is incomplete or invalid.", pushMessage: pushMessage)
+            return
+        }
+
+        let durationInMinutes = Int(durationValue)
+        let pushMessageDate = Date(timeIntervalSince1970: pushMessage.timestamp)
+
+        let tempTarget = TempTarget(
+            name: TempTarget.custom,
+            createdAt: pushMessageDate,
+            targetTop: Decimal(targetValue),
+            targetBottom: Decimal(targetValue),
+            duration: Decimal(durationInMinutes),
+            enteredBy: TempTarget.manual,
+            reason: TempTarget.custom
+        )
+
+        tempTargetsStorage.storeTempTargets([tempTarget])
+
+        debug(
+            .remoteControl,
+            "Remote command processed successfully. \(pushMessage.humanReadableDescription())"
+        )
+    }
+
+    func cancelTempTarget(_ pushMessage: PushMessage) async {
+        debug(.remoteControl, "Cancelling temp target.")
+
+        guard tempTargetsStorage.current() != nil else {
+            await logError("Command rejected: no active temp target to cancel.")
+            return
+        }
+
+        let cancelEntry = TempTarget.cancel(at: Date())
+        tempTargetsStorage.storeTempTargets([cancelEntry])
+
+        debug(
+            .remoteControl,
+            "Remote command processed successfully. \(pushMessage.humanReadableDescription())"
+        )
+    }
+}

+ 113 - 0
FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl.swift

@@ -0,0 +1,113 @@
+import CoreData
+import Foundation
+import Swinject
+
+class TrioRemoteControl: Injectable {
+    static let shared = TrioRemoteControl()
+
+    @Injected() internal var tempTargetsStorage: TempTargetsStorage!
+    @Injected() internal var carbsStorage: CarbsStorage!
+    @Injected() internal var nightscoutManager: NightscoutManager!
+    @Injected() internal var overrideStorage: OverrideStorage!
+    @Injected() internal var settings: SettingsManager!
+
+    private let timeWindow: TimeInterval = 600 // Defines how old messages that are accepted, 10 minutes
+
+    internal let pumpHistoryFetchContext: NSManagedObjectContext
+    internal let viewContext: NSManagedObjectContext
+
+    private init() {
+        pumpHistoryFetchContext = CoreDataStack.shared.newTaskContext()
+        viewContext = CoreDataStack.shared.persistentContainer.viewContext
+        injectServices(FreeAPSApp.resolver)
+    }
+
+    func handleRemoteNotification(pushMessage: PushMessage) async {
+        let isTrioRemoteControlEnabled = UserDefaults.standard.bool(forKey: "isTrioRemoteControlEnabled")
+        guard isTrioRemoteControlEnabled else {
+            await logError("Remote command received, but remote control is disabled in settings. Ignoring the command.")
+            return
+        }
+
+        let currentTime = Date().timeIntervalSince1970
+        let timeDifference = currentTime - pushMessage.timestamp
+
+        if timeDifference > timeWindow {
+            await logError(
+                "Command rejected: the message is too old (sent \(Int(timeDifference)) seconds ago, which exceeds the allowed limit).",
+                pushMessage: pushMessage
+            )
+            return
+        } else if timeDifference < -timeWindow {
+            await logError(
+                "Command rejected: the message has an invalid future timestamp (timestamp is \(Int(-timeDifference)) seconds ahead of the current time).",
+                pushMessage: pushMessage
+            )
+            return
+        }
+
+        debug(.remoteControl, "Command received with acceptable time difference: \(Int(timeDifference)) seconds.")
+
+        let storedSecret = UserDefaults.standard.string(forKey: "trioRemoteControlSharedSecret") ?? ""
+        guard !storedSecret.isEmpty else {
+            await logError(
+                "Command rejected: shared secret is missing in settings. Cannot authenticate the command.",
+                pushMessage: pushMessage
+            )
+            return
+        }
+
+        guard pushMessage.sharedSecret == storedSecret else {
+            await logError(
+                "Command rejected: shared secret does not match. Cannot authenticate the command.",
+                pushMessage: pushMessage
+            )
+            return
+        }
+
+        switch pushMessage.commandType {
+        case .bolus:
+            await handleBolusCommand(pushMessage)
+        case .tempTarget:
+            await handleTempTargetCommand(pushMessage)
+        case .cancelTempTarget:
+            await cancelTempTarget(pushMessage)
+        case .meal:
+            await handleMealCommand(pushMessage)
+        case .startOverride:
+            await handleStartOverrideCommand(pushMessage)
+        case .cancelOverride:
+            await handleCancelOverrideCommand(pushMessage)
+        }
+    }
+}
+
+// MARK: - CommandType Enum
+
+extension TrioRemoteControl {
+    enum CommandType: String, Codable {
+        case bolus
+        case tempTarget = "temp_target"
+        case cancelTempTarget = "cancel_temp_target"
+        case meal
+        case startOverride = "start_override"
+        case cancelOverride = "cancel_override"
+
+        var description: String {
+            switch self {
+            case .bolus:
+                return "Bolus"
+            case .tempTarget:
+                return "Temporary Target"
+            case .cancelTempTarget:
+                return "Cancel Temporary Target"
+            case .meal:
+                return "Meal"
+            case .startOverride:
+                return "Start Override"
+            case .cancelOverride:
+                return "Cancel Override"
+            }
+        }
+    }
+}

+ 6 - 0
FreeAPS/Sources/Modules/RemoteControlConfig/RemoteControlConfigDataFlow.swift

@@ -0,0 +1,6 @@
+import Foundation
+enum RemoteControlConfig {
+    enum Config {}
+}
+
+protocol RemoteControlConfigProvider {}

+ 4 - 0
FreeAPS/Sources/Modules/RemoteControlConfig/RemoteControlConfigProvider.swift

@@ -0,0 +1,4 @@
+import Foundation
+extension RemoteControlConfig {
+    final class Provider: BaseProvider, RemoteControlConfigProvider {}
+}

+ 47 - 0
FreeAPS/Sources/Modules/RemoteControlConfig/RemoteControlConfigStateModel.swift

@@ -0,0 +1,47 @@
+import SwiftUI
+
+extension RemoteControlConfig {
+    final class StateModel: BaseStateModel<Provider> {
+        @Published var units: GlucoseUnits = .mgdL
+        @Published var isTrioRemoteControlEnabled: Bool = false
+        @Published var sharedSecret: String = ""
+
+        override func subscribe() {
+            units = settingsManager.settings.units
+            isTrioRemoteControlEnabled = UserDefaults.standard.bool(forKey: "isTrioRemoteControlEnabled")
+            sharedSecret = UserDefaults.standard.string(forKey: "trioRemoteControlSharedSecret") ?? generateInitialSharedSecret()
+
+            $isTrioRemoteControlEnabled
+                .receive(on: DispatchQueue.main)
+                .sink { value in
+                    UserDefaults.standard.set(value, forKey: "isTrioRemoteControlEnabled")
+                }
+                .store(in: &lifetime)
+
+            $sharedSecret
+                .receive(on: DispatchQueue.main)
+                .sink { value in
+                    UserDefaults.standard.set(value, forKey: "trioRemoteControlSharedSecret")
+                }
+                .store(in: &lifetime)
+        }
+
+        func generateNewSharedSecret() {
+            let newSecret = UUID().uuidString.replacingOccurrences(of: "-", with: "")
+            sharedSecret = newSecret
+            UserDefaults.standard.set(newSecret, forKey: "trioRemoteControlSharedSecret")
+        }
+
+        private func generateInitialSharedSecret() -> String {
+            let secret = UUID().uuidString.replacingOccurrences(of: "-", with: "")
+            UserDefaults.standard.set(secret, forKey: "trioRemoteControlSharedSecret")
+            return secret
+        }
+    }
+}
+
+extension RemoteControlConfig.StateModel: SettingsObserver {
+    func settingsDidChange(_: FreeAPSSettings) {
+        units = settingsManager.settings.units
+    }
+}

+ 113 - 0
FreeAPS/Sources/Modules/RemoteControlConfig/View/RemoteControlConfig.swift

@@ -0,0 +1,113 @@
+import Combine
+import SwiftUI
+import Swinject
+import UIKit
+
+extension RemoteControlConfig {
+    struct RootView: BaseView {
+        let resolver: Resolver
+
+        @StateObject var state = StateModel()
+
+        @State private var shouldDisplayHint: Bool = false
+        @State var hintDetent = PresentationDetent.large
+        @State var selectedVerboseHint: String?
+        @State var hintLabel: String?
+        @State private var decimalPlaceholder: Decimal = 0.0
+        @State private var isCopied: Bool = false
+
+        @Environment(\.colorScheme) 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
+                )
+        }
+
+        var body: some View {
+            Form {
+                SettingInputSection(
+                    decimalValue: $decimalPlaceholder,
+                    booleanValue: $state.isTrioRemoteControlEnabled,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    selectedVerboseHint: Binding(
+                        get: { selectedVerboseHint },
+                        set: {
+                            selectedVerboseHint = $0
+                            hintLabel = "Enable Remote Command"
+                        }
+                    ),
+                    units: state.units,
+                    type: .boolean,
+                    label: "Enable Remote Control",
+                    miniHint: "Remote Control allow Trio to receive instructions, such as boluses and temp targets, from LoopFollow.",
+                    verboseHint: "When Remote Control is enabled, you can send boluses, overrides, temporary targets, carbs, and other commands to Trio via push notifications. To ensure security, these commands are protected by a shared secret, which must be entered in LoopFollow.",
+                    headerText: "Trio Remote Control"
+                )
+
+                Section(
+                    header: Text("Shared Secret"),
+                    content: {
+                        TextField("Enter Shared Secret", text: $state.sharedSecret)
+                            .disableAutocorrection(true)
+                            .autocapitalization(.none)
+                            .padding(8)
+                            .background(Color(UIColor.systemGray6))
+                            .cornerRadius(8)
+
+                        Button(action: {
+                            UIPasteboard.general.string = state.sharedSecret
+                            isCopied = true
+                        }) {
+                            Label("Copy Secret", systemImage: "doc.on.doc")
+                                .frame(maxWidth: .infinity)
+                        }
+                        .buttonStyle(.bordered)
+                        .frame(maxWidth: .infinity)
+                        .alert(isPresented: $isCopied) {
+                            Alert(
+                                title: Text("Copied"),
+                                message: Text("Shared Secret copied to clipboard"),
+                                dismissButton: .default(Text("OK"))
+                            )
+                        }
+
+                        Button(action: {
+                            state.generateNewSharedSecret()
+                        }) {
+                            Label("Generate Secret", systemImage: "arrow.clockwise")
+                                .frame(maxWidth: .infinity)
+                        }
+                        .buttonStyle(.borderedProminent)
+                        .foregroundColor(.white)
+                        .frame(maxWidth: .infinity)
+                    }
+                ).listRowBackground(Color.chart)
+            }
+            .sheet(isPresented: $shouldDisplayHint) {
+                SettingInputHintView(
+                    hintDetent: $hintDetent,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    hintLabel: hintLabel ?? "",
+                    hintText: selectedVerboseHint ?? "",
+                    sheetTitle: "Help"
+                )
+            }
+            .scrollContentBackground(.hidden).background(color)
+            .onAppear(perform: configureView)
+            .navigationTitle("Remote Control")
+            .navigationBarTitleDisplayMode(.automatic)
+        }
+    }
+}

+ 6 - 0
FreeAPS/Sources/Modules/Settings/SettingItems.swift

@@ -175,6 +175,12 @@ enum SettingItems {
             path: ["Features", "Shortcuts"]
             path: ["Features", "Shortcuts"]
         ),
         ),
         SettingItem(
         SettingItem(
+            title: "Remote Control",
+            view: .remoteControlConfig,
+            searchContents: ["Remote Control"],
+            path: ["Features", "Remote Control"]
+        ),
+        SettingItem(
             title: "User Interface",
             title: "User Interface",
             view: .userInterfaceSettings,
             view: .userInterfaceSettings,
             searchContents: [
             searchContents: [

+ 1 - 0
FreeAPS/Sources/Modules/Settings/View/Subviews/FeatureSettingsView.swift

@@ -39,6 +39,7 @@ struct FeatureSettingsView: BaseView {
                     Text("Bolus Calculator").navigationLink(to: .bolusCalculatorConfig, from: self)
                     Text("Bolus Calculator").navigationLink(to: .bolusCalculatorConfig, from: self)
                     Text("Meal Settings").navigationLink(to: .mealSettings, from: self)
                     Text("Meal Settings").navigationLink(to: .mealSettings, from: self)
                     Text("Shortcuts").navigationLink(to: .shortcutsConfig, from: self)
                     Text("Shortcuts").navigationLink(to: .shortcutsConfig, from: self)
+                    Text("Remote Control").navigationLink(to: .remoteControlConfig, from: self)
                 }
                 }
             )
             )
             .listRowBackground(Color.chart)
             .listRowBackground(Color.chart)

+ 3 - 0
FreeAPS/Sources/Router/Screen.swift

@@ -43,6 +43,7 @@ enum Screen: Identifiable, Hashable {
     case liveActivityBottomRowSettings
     case liveActivityBottomRowSettings
     case calendarEventSettings
     case calendarEventSettings
     case serviceSettings
     case serviceSettings
+    case remoteControlConfig
     case autosensSettings
     case autosensSettings
     case smbSettings
     case smbSettings
     case targetBehavior
     case targetBehavior
@@ -119,6 +120,8 @@ extension Screen {
             Calibrations.RootView(resolver: resolver)
             Calibrations.RootView(resolver: resolver)
         case .shortcutsConfig:
         case .shortcutsConfig:
             ShortcutsConfig.RootView(resolver: resolver)
             ShortcutsConfig.RootView(resolver: resolver)
+        case .remoteControlConfig:
+            RemoteControlConfig.RootView(resolver: resolver)
         case .devices:
         case .devices:
             DevicesView(resolver: resolver, state: Settings.StateModel())
             DevicesView(resolver: resolver, state: Settings.StateModel())
         case .therapySettings:
         case .therapySettings:

+ 44 - 9
FreeAPS/Sources/Services/Network/NightscoutManager.swift

@@ -19,6 +19,7 @@ protocol NightscoutManager: GlucoseSource {
     func uploadProfiles() async
     func uploadProfiles() async
     func importSettings() async -> ScheduledNightscoutProfile?
     func importSettings() async -> ScheduledNightscoutProfile?
     var cgmURL: URL? { get }
     var cgmURL: URL? { get }
+    func uploadNoteTreatment(note: String) async
 }
 }
 
 
 final class BaseNightscoutManager: NightscoutManager, Injectable {
 final class BaseNightscoutManager: NightscoutManager, Injectable {
@@ -36,6 +37,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     @Injected() private var reachabilityManager: ReachabilityManager!
     @Injected() private var reachabilityManager: ReachabilityManager!
     @Injected() var healthkitManager: HealthKitManager!
     @Injected() var healthkitManager: HealthKitManager!
 
 
+    private let uploadOverridesSubject = PassthroughSubject<Void, Never>()
     private let processQueue = DispatchQueue(label: "BaseNetworkManager.processQueue")
     private let processQueue = DispatchQueue(label: "BaseNetworkManager.processQueue")
     private var ping: TimeInterval?
     private var ping: TimeInterval?
 
 
@@ -95,6 +97,16 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             }
             }
             .store(in: &subscriptions)
             .store(in: &subscriptions)
 
 
+        uploadOverridesSubject
+            .debounce(for: .seconds(1), scheduler: DispatchQueue.global(qos: .background))
+            .sink { [weak self] in
+                guard let self = self else { return }
+                Task {
+                    await self.uploadOverrides()
+                }
+            }
+            .store(in: &subscriptions)
+
         registerHandlers()
         registerHandlers()
         setupNotification()
         setupNotification()
     }
     }
@@ -116,17 +128,11 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         }.store(in: &subscriptions)
         }.store(in: &subscriptions)
 
 
         coreDataPublisher?.filterByEntityName("OverrideStored").sink { [weak self] _ in
         coreDataPublisher?.filterByEntityName("OverrideStored").sink { [weak self] _ in
-            guard let self = self else { return }
-            Task.detached {
-                await self.uploadOverrides()
-            }
+            self?.uploadOverridesSubject.send()
         }.store(in: &subscriptions)
         }.store(in: &subscriptions)
 
 
         coreDataPublisher?.filterByEntityName("OverrideRunStored").sink { [weak self] _ in
         coreDataPublisher?.filterByEntityName("OverrideRunStored").sink { [weak self] _ in
-            guard let self = self else { return }
-            Task.detached {
-                await self.uploadOverrides()
-            }
+            self?.uploadOverridesSubject.send()
         }.store(in: &subscriptions)
         }.store(in: &subscriptions)
 
 
         coreDataPublisher?.filterByEntityName("PumpEventStored").sink { [weak self] _ in
         coreDataPublisher?.filterByEntityName("PumpEventStored").sink { [weak self] _ in
@@ -619,13 +625,25 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                 let defaultProfile = "default"
                 let defaultProfile = "default"
 
 
                 let now = Date()
                 let now = Date()
+
+                let bundleIdentifier = Bundle.main.bundleIdentifier ?? ""
+                let deviceToken = UserDefaults.standard.string(forKey: "deviceToken") ?? ""
+                let isAPNSProduction = UserDefaults.standard.bool(forKey: "isAPNSProduction")
+                let presetOverrides = await overridesStorage.getPresetOverridesForNightscout()
+                let teamID = Bundle.main.object(forInfoDictionaryKey: "TeamID") as? String ?? ""
+
                 let profileStore = NightscoutProfileStore(
                 let profileStore = NightscoutProfileStore(
                     defaultProfile: defaultProfile,
                     defaultProfile: defaultProfile,
                     startDate: now,
                     startDate: now,
                     mills: Int(now.timeIntervalSince1970) * 1000,
                     mills: Int(now.timeIntervalSince1970) * 1000,
                     units: nsUnits,
                     units: nsUnits,
                     enteredBy: NightscoutTreatment.local,
                     enteredBy: NightscoutTreatment.local,
-                    store: [defaultProfile: scheduledProfile]
+                    store: [defaultProfile: scheduledProfile],
+                    bundleIdentifier: bundleIdentifier,
+                    deviceToken: deviceToken,
+                    isAPNSProduction: isAPNSProduction,
+                    overridePresets: presetOverrides,
+                    teamID: teamID
                 )
                 )
 
 
                 guard let nightscout = nightscoutAPI, isNetworkReachable else {
                 guard let nightscout = nightscoutAPI, isNetworkReachable else {
@@ -944,6 +962,23 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             }
             }
         }
         }
     }
     }
+
+    func uploadNoteTreatment(note: String) async {
+        let uploadedNotes = storage.retrieve(OpenAPS.Nightscout.uploadedNotes, as: [NightscoutTreatment].self) ?? []
+        let now = Date()
+
+        if uploadedNotes.last?.notes != note || (uploadedNotes.last?.createdAt ?? .distantPast) != now {
+            let noteTreatment = NightscoutTreatment(
+                eventType: .nsNote,
+                createdAt: now,
+                enteredBy: NightscoutTreatment.local,
+                notes: note,
+                targetTop: nil,
+                targetBottom: nil
+            )
+            await uploadTreatments([noteTreatment], fileToSave: OpenAPS.Nightscout.uploadedNotes)
+        }
+    }
 }
 }
 
 
 extension Array {
 extension Array {

+ 18 - 15
FreeAPS/Sources/Services/WatchManager/WatchManager.swift

@@ -195,28 +195,30 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
 
 
     @MainActor private func configureState() async {
     @MainActor private func configureState() async {
         let glucoseValuesIds = await fetchGlucose()
         let glucoseValuesIds = await fetchGlucose()
-        async let lastDeterminationIds = fetchlastDetermination()
-        async let latestOverrideId = fetchLatestOverride()
+        async let getLatestDeterminationIds = fetchlastDetermination()
+        async let getlatestOverrideId = fetchLatestOverride()
 
 
-        guard let lastDeterminationId = await lastDeterminationIds.first,
-              let latestOverrideId = await latestOverrideId
-        else {
-            debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to get last Determination/ last Override")
+        let latestOverrideId = await getlatestOverrideId
+
+        guard let lastDeterminationId = await getLatestDeterminationIds.first else {
+            debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to get last Determination")
             return
             return
         }
         }
 
 
         do {
         do {
             let glucoseValues: [GlucoseStored] = await CoreDataStack.shared
             let glucoseValues: [GlucoseStored] = await CoreDataStack.shared
                 .getNSManagedObject(with: glucoseValuesIds, context: viewContext)
                 .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 recommendedInsulin = await newBolusCalc(
             let recommendedInsulin = await newBolusCalc(
                 glucoseIds: glucoseValuesIds,
                 glucoseIds: glucoseValuesIds,
                 determinationId: lastDeterminationId
                 determinationId: lastDeterminationId
             )
             )
 
 
+            var latestOverride: OverrideStored?
+            if let id = latestOverrideId {
+                latestOverride = try viewContext.existingObject(with: id) as? OverrideStored
+            }
+
             await MainActor.run { [weak self] in
             await MainActor.run { [weak self] in
                 guard let self = self else { return }
                 guard let self = self else { return }
 
 
@@ -296,12 +298,13 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
 
 
                 self.state.isf = lastDetermination?.insulinSensitivity as? Decimal
                 self.state.isf = lastDetermination?.insulinSensitivity as? Decimal
 
 
-                if latestOverride?.enabled ?? false {
-                    let percentString = "\((latestOverride?.percentage ?? 100).formatted(.number)) %"
-                    self.state.override = percentString
-
-                } else {
-                    self.state.override = "100 %"
+                if let latestOverride = latestOverride {
+                    if latestOverride.enabled {
+                        let percentString = "\(latestOverride.percentage.formatted(.number)) %"
+                        self.state.override = percentString
+                    } else {
+                        self.state.override = "100 %"
+                    }
                 }
                 }
 
 
                 self.sendState()
                 self.sendState()

+ 1 - 1
FreeAPS/Sources/Shortcuts/AppShortcuts.swift

@@ -25,7 +25,7 @@ import Foundation
             ]
             ]
         )
         )
         AppShortcut(
         AppShortcut(
-            intent: AddCarbPresentIntent(),
+            intent: AddCarbPresetIntent(),
             phrases: [
             phrases: [
                 "Add carbs in \(.applicationName)",
                 "Add carbs in \(.applicationName)",
                 "\(.applicationName) allows to add carbs"
                 "\(.applicationName) allows to add carbs"

+ 1 - 1
FreeAPS/Sources/Shortcuts/Carbs/AddCarbPresetIntent.swift

@@ -3,7 +3,7 @@ import Foundation
 import Intents
 import Intents
 import Swinject
 import Swinject
 
 
-@available(iOS 16.0,*) struct AddCarbPresentIntent: AppIntent {
+@available(iOS 16.0,*) struct AddCarbPresetIntent: AppIntent {
     // Title of the action in the Shortcuts app
     // Title of the action in the Shortcuts app
     static var title: LocalizedStringResource = "Add carbs"
     static var title: LocalizedStringResource = "Add carbs"
 
 

+ 2 - 2
Model/TrioCoreDataPersistentContainer.xcdatamodeld/TrioCoreDataPersistentContainer.xcdatamodel/contents

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22757" systemVersion="23G93" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
+<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23231" systemVersion="24A348" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
     <entity name="BolusStored" representedClassName="BolusStored" syncable="YES">
     <entity name="BolusStored" representedClassName="BolusStored" syncable="YES">
         <attribute name="amount" optional="YES" attributeType="Decimal" defaultValueString="0"/>
         <attribute name="amount" optional="YES" attributeType="Decimal" defaultValueString="0"/>
         <attribute name="isExternal" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
         <attribute name="isExternal" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
@@ -19,7 +19,7 @@
         <attribute name="note" optional="YES" attributeType="String"/>
         <attribute name="note" optional="YES" attributeType="String"/>
         <attribute name="protein" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
         <attribute name="protein" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
         <fetchIndex name="byDate">
         <fetchIndex name="byDate">
-            <fetchIndexElement property="date" type="Binary" order="ascending"/>
+            <fetchIndexElement property="date" type="Binary" order="descending"/>
         </fetchIndex>
         </fetchIndex>
         <fetchIndex name="byIsFPU">
         <fetchIndex name="byIsFPU">
             <fetchIndexElement property="isFPU" type="Binary" order="ascending"/>
             <fetchIndexElement property="isFPU" type="Binary" order="ascending"/>

+ 1 - 1
README.md

@@ -63,7 +63,7 @@ Instructions in greater detail, but not Trio-specific:
 
 
 # Documentation
 # Documentation
 
 
-[Discord Trio - Server ](https://discord.gg/KepAG6RdYZ)
+[Discord Trio - Server ](http://discord.diy-trio.org)
 
 
 [Trio documentation](https://docs.diy-trio.org/en/latest/)
 [Trio documentation](https://docs.diy-trio.org/en/latest/)
 
 

+ 2 - 1
fastlane/Fastfile

@@ -187,7 +187,8 @@ platform :ios do
     configure_bundle_id("Trio", "#{BUNDLE_ID}", [
     configure_bundle_id("Trio", "#{BUNDLE_ID}", [
       Spaceship::ConnectAPI::BundleIdCapability::Type::APP_GROUPS,
       Spaceship::ConnectAPI::BundleIdCapability::Type::APP_GROUPS,
       Spaceship::ConnectAPI::BundleIdCapability::Type::HEALTHKIT,
       Spaceship::ConnectAPI::BundleIdCapability::Type::HEALTHKIT,
-      Spaceship::ConnectAPI::BundleIdCapability::Type::NFC_TAG_READING
+      Spaceship::ConnectAPI::BundleIdCapability::Type::NFC_TAG_READING,
+      Spaceship::ConnectAPI::BundleIdCapability::Type::PUSH_NOTIFICATIONS
     ])
     ])
 
 
     configure_bundle_id("Trio WatchKit Extension", "#{BUNDLE_ID}.watchkitapp.watchkitextension", [
     configure_bundle_id("Trio WatchKit Extension", "#{BUNDLE_ID}.watchkitapp.watchkitextension", [