Explorar o código

Merge branch 'core-data-sync-trio' of github.com:dnzxy/Trio-dev into tempTargets

Deniz Cengiz hai 1 ano
pai
achega
6010b4645a
Modificáronse 45 ficheiros con 1202 adicións e 60 borrados
  1. 1 1
      CGMBLEKit
  2. 1 1
      CODE_OF_CONDUCT.md
  3. 1 1
      Config.xcconfig
  4. 72 0
      FreeAPS.xcodeproj/project.pbxproj
  5. 2 0
      FreeAPS/Resources/FreeAPS.entitlements
  6. 2 0
      FreeAPS/Resources/Info.plist
  7. 11 1
      FreeAPS/Sources/APS/FetchGlucoseManager.swift
  8. 1 0
      FreeAPS/Sources/APS/OpenAPS/Constants.swift
  9. 76 7
      FreeAPS/Sources/APS/OpenAPS/JavaScriptWorker.swift
  10. 2 2
      FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift
  11. 28 0
      FreeAPS/Sources/APS/Storage/OverrideStorage.swift
  12. 51 1
      FreeAPS/Sources/Application/AppDelegate.swift
  13. 4 0
      FreeAPS/Sources/Logger/Logger.swift
  14. 12 0
      FreeAPS/Sources/Models/NightscoutStatus.swift
  15. 1 0
      FreeAPS/Sources/Models/PumpHistoryEvent.swift
  16. 141 0
      FreeAPS/Sources/Models/PushMessage.swift
  17. 7 7
      FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift
  18. 16 0
      FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift
  19. 9 0
      FreeAPS/Sources/Modules/OverrideConfig/OverrideStateModel.swift
  20. 5 4
      FreeAPS/Sources/Modules/OverrideConfig/View/AddOverrideForm.swift
  21. 4 3
      FreeAPS/Sources/Modules/OverrideConfig/View/EditOverrideForm.swift
  22. 36 0
      FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+APNS.swift
  23. 108 0
      FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+Bolus.swift
  24. 12 0
      FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+Helpers.swift
  25. 82 0
      FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+Meal.swift
  26. 100 0
      FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+Override.swift
  27. 53 0
      FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+TempTarget.swift
  28. 113 0
      FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl.swift
  29. 6 0
      FreeAPS/Sources/Modules/RemoteControlConfig/RemoteControlConfigDataFlow.swift
  30. 4 0
      FreeAPS/Sources/Modules/RemoteControlConfig/RemoteControlConfigProvider.swift
  31. 47 0
      FreeAPS/Sources/Modules/RemoteControlConfig/RemoteControlConfigStateModel.swift
  32. 113 0
      FreeAPS/Sources/Modules/RemoteControlConfig/View/RemoteControlConfig.swift
  33. 6 0
      FreeAPS/Sources/Modules/Settings/SettingItems.swift
  34. 1 0
      FreeAPS/Sources/Modules/Settings/View/Subviews/FeatureSettingsView.swift
  35. 3 0
      FreeAPS/Sources/Router/Screen.swift
  36. 44 9
      FreeAPS/Sources/Services/Network/NightscoutManager.swift
  37. 18 15
      FreeAPS/Sources/Services/WatchManager/WatchManager.swift
  38. 1 1
      FreeAPS/Sources/Shortcuts/AppShortcuts.swift
  39. 1 1
      FreeAPS/Sources/Shortcuts/Carbs/AddCarbPresetIntent.swift
  40. 1 1
      G7SensorKit
  41. 1 1
      Model/TrioCoreDataPersistentContainer.xcdatamodeld/TrioCoreDataPersistentContainer.xcdatamodel/contents
  42. 1 1
      OmniBLE
  43. 1 1
      OmniKit
  44. 1 1
      README.md
  45. 2 1
      fastlane/Fastfile

+ 1 - 1
CGMBLEKit

@@ -1 +1 @@
-Subproject commit 15af9cf319bff2ac49c361da254ad667461d4687
+Subproject commit b786e8b5531cb08c259103c472dcd6a6752728f8

+ 1 - 1
CODE_OF_CONDUCT.md

@@ -59,7 +59,7 @@ representative at an online or offline event.
 ## Enforcement
 
 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
 investigated promptly and fairly.
 

+ 1 - 1
Config.xcconfig

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

+ 72 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -450,6 +450,12 @@
 		DD1745552C55CA6C00211FAC /* UnitsLimitsSettingsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1745542C55CA6C00211FAC /* UnitsLimitsSettingsRootView.swift */; };
 		DD1DB7CC2BECCA1F0048B367 /* BuildDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1DB7CB2BECCA1F0048B367 /* BuildDetails.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 */; };
 		DD6B7CB22C7B6F0800B75029 /* Rounding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CB12C7B6F0800B75029 /* Rounding.swift */; };
 		DD6B7CB42C7B71F700B75029 /* ForecastDisplayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */; };
@@ -460,6 +466,12 @@
 		DD88C8E22C50420800F2D558 /* DefinitionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD88C8E12C50420800F2D558 /* DefinitionRow.swift */; };
 		DD940BAA2CA7585D000830A5 /* GlucoseColorScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD940BA92CA7585D000830A5 /* GlucoseColorScheme.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 */; };
 		DDD163122C4C689900CD525A /* OverrideStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163112C4C689900CD525A /* OverrideStateModel.swift */; };
 		DDD163142C4C68D300CD525A /* OverrideProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163132C4C68D300CD525A /* OverrideProvider.swift */; };
@@ -1121,6 +1133,12 @@
 		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>"; };
 		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>"; };
 		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>"; };
@@ -1131,6 +1149,12 @@
 		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>"; };
 		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>"; };
 		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>"; };
@@ -1485,6 +1509,8 @@
 		3811DE0325C9D31700A708ED /* Modules */ = {
 			isa = PBXGroup;
 			children = (
+				DD9ECB6B2CA99FA400AA7C45 /* RemoteControlConfig */,
+				DD9ECB662CA99EFE00AA7C45 /* RemoteControl */,
 				DD1745382C55BF8B00211FAC /* AlgorithmAdvancedSettings */,
 				DD1745422C55C5C400211FAC /* AutosensSettings */,
 				672F63EEAE27400625E14BAD /* AutotuneConfig */,
@@ -1977,6 +2003,7 @@
 				DD21FCB42C6952AD00AF2C25 /* DecimalPickerSettings.swift */,
 				DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */,
 				DD6B7CB52C7B748B00B75029 /* TotalInsulinDisplayType.swift */,
+				DD9ECB692CA99F6C00AA7C45 /* PushMessage.swift */,
 			);
 			path = Models;
 			sourceTree = "<group>";
@@ -2715,6 +2742,39 @@
 			path = ProfileImport;
 			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 */ = {
 			isa = PBXGroup;
 			children = (
@@ -3213,6 +3273,7 @@
 				3811DEEB25CA063400A708ED /* PersistedProperty.swift in Sources */,
 				38E44537274E411700EC9A94 /* Disk+Helpers.swift in Sources */,
 				388E5A6025B6F2310019842D /* Autosens.swift in Sources */,
+				DD9ECB6A2CA99F6C00AA7C45 /* PushMessage.swift in Sources */,
 				3811DE8F25C9D80400A708ED /* User.swift in Sources */,
 				5825A1BE2C97335C0046467E /* EditTempTargetForm.swift in Sources */,
 				19D466A329AA2B80004D5F33 /* MealSettingsDataFlow.swift in Sources */,
@@ -3249,6 +3310,7 @@
 				DD1745402C55BFC100211FAC /* AlgorithmAdvancedSettingsRootView.swift in Sources */,
 				58645BA52CA2D347008AFCE7 /* ForecastSetup.swift in Sources */,
 				110AEDEE2C51A0AE00615CC9 /* ShortcutsConfigStateModel.swift in Sources */,
+				DD9ECB722CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift in Sources */,
 				DD1745262C55526F00211FAC /* SMBSettingsRootView.swift in Sources */,
 				3811DE3025C9D49500A708ED /* HomeStateModel.swift in Sources */,
 				38BF021725E7CBBC00579895 /* PumpManagerExtensions.swift in Sources */,
@@ -3307,8 +3369,10 @@
 				3811DF0225CA9FEA00A708ED /* Credentials.swift in Sources */,
 				5837A5302BD2E3C700A5DC04 /* CarbEntryStored+helper.swift in Sources */,
 				389A572026079BAA00BC102F /* Interpolation.swift in Sources */,
+				DD9ECB702CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift in Sources */,
 				19A910382A24EF3200C8951B /* ChartsView.swift in Sources */,
 				58A3D5522C96EFA8003F90FC /* TempTargetStored+CoreDataProperties.swift in Sources */,
+				DD32CF9A2CC8247B003686D6 /* TrioRemoteControl+Meal.swift in Sources */,
 				BDF34F832C10C5B600D51995 /* DataManager.swift in Sources */,
 				38B4F3C625E5017E00E76A18 /* NotificationCenter.swift in Sources */,
 				19D466A729AA2C22004D5F33 /* MealSettingsStateModel.swift in Sources */,
@@ -3338,6 +3402,7 @@
 				388358C825EEF6D200E024B2 /* BasalProfileEntry.swift in Sources */,
 				3811DE0B25C9D32F00A708ED /* BaseView.swift in Sources */,
 				3811DE3225C9D49500A708ED /* HomeDataFlow.swift in Sources */,
+				DD32CFA22CC824E2003686D6 /* TrioRemoteControl+Helpers.swift in Sources */,
 				CE1856F52ADC4858007E39C7 /* AddCarbPresetIntent.swift in Sources */,
 				38569347270B5DFB0002C50D /* CGMType.swift in Sources */,
 				3821ED4C25DD18BA00BC42AD /* Constants.swift in Sources */,
@@ -3388,6 +3453,7 @@
 				CE7950242997D81700FA576E /* CGMSettingsView.swift in Sources */,
 				58237D9E2BCF0A6B00A47A79 /* PopupView.swift in Sources */,
 				38D0B3D925EC07C400CB6E88 /* CarbsEntry.swift in Sources */,
+				DD32CF9C2CC82499003686D6 /* TrioRemoteControl+TempTarget.swift in Sources */,
 				38A9260525F012D8009E3739 /* CarbRatios.swift in Sources */,
 				38FCF3D625E8FDF40078B0D1 /* MD5.swift in Sources */,
 				DDD163142C4C68D300CD525A /* OverrideProvider.swift in Sources */,
@@ -3448,6 +3514,7 @@
 				72F1BD388F42FCA6C52E4500 /* ConfigEditorProvider.swift in Sources */,
 				E39E418C56A5A46B61D960EE /* ConfigEditorStateModel.swift in Sources */,
 				45717281F743594AA9D87191 /* ConfigEditorRootView.swift in Sources */,
+				DD32CFA02CC824D6003686D6 /* TrioRemoteControl+APNS.swift in Sources */,
 				CE7CA3532A064973004BE681 /* tempPresetIntent.swift in Sources */,
 				D6DEC113821A7F1056C4AA1E /* NightscoutConfigDataFlow.swift in Sources */,
 				DD6B7CBB2C7FBBFA00B75029 /* ReviewInsulinActionView.swift in Sources */,
@@ -3498,6 +3565,7 @@
 				CE7CA3512A064973004BE681 /* ApplyTempPresetIntent.swift in Sources */,
 				FA630397F76B582C8D8681A7 /* BasalProfileEditorProvider.swift in Sources */,
 				DD1745172C54389F00211FAC /* FeatureSettingsView.swift in Sources */,
+				DD9ECB712CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift in Sources */,
 				63E890B4D951EAA91C071D5C /* BasalProfileEditorStateModel.swift in Sources */,
 				38FEF3FA2737E42000574A46 /* BaseStateModel.swift in Sources */,
 				CC6C406E2ACDD69E009B8058 /* RawFetchedProfile.swift in Sources */,
@@ -3526,6 +3594,7 @@
 				38192E0D261BAF980094D973 /* ConvenienceExtensions.swift in Sources */,
 				88AB39B23C9552BD6E0C9461 /* ISFEditorRootView.swift in Sources */,
 				BD6EB2D62C7D049B0086BBB6 /* LiveActivityWidgetConfiguration.swift in Sources */,
+				DD32CF982CC82463003686D6 /* TrioRemoteControl+Bolus.swift in Sources */,
 				F816825E28DB441200054060 /* HeartBeatManager.swift in Sources */,
 				58F107742BD1A4D000B1A680 /* Determination+helper.swift in Sources */,
 				38FEF413273B317A00574A46 /* HKUnit.swift in Sources */,
@@ -3571,9 +3640,11 @@
 				38A00B1F25FC00F7006BC0B0 /* Autotune.swift in Sources */,
 				38AAF85525FFF846004AF583 /* CurrentGlucoseView.swift in Sources */,
 				041D1E995A6AE92E9289DC49 /* BolusDataFlow.swift in Sources */,
+				DD32CF9E2CC824C5003686D6 /* TrioRemoteControl+Override.swift in Sources */,
 				23888883D4EA091C88480FF2 /* BolusProvider.swift in Sources */,
 				38E98A2D25F52DC400C0CED0 /* NSLocking+Extensions.swift in Sources */,
 				BDBAACFA2C2D439700370AAE /* OverrideData.swift in Sources */,
+				DD9ECB682CA99F4500AA7C45 /* TrioRemoteControl.swift in Sources */,
 				38569353270B5E350002C50D /* CGMRootView.swift in Sources */,
 				69A31254F2451C20361D172F /* BolusStateModel.swift in Sources */,
 				1967DFC029D053AC00759F30 /* IconSelection.swift in Sources */,
@@ -3657,6 +3728,7 @@
 				DDE1796E2C910127003CDDB7 /* OrefDetermination+CoreDataClass.swift in Sources */,
 				DDE1796F2C910127003CDDB7 /* OrefDetermination+CoreDataProperties.swift in Sources */,
 				DDE179702C910127003CDDB7 /* OverrideStored+CoreDataClass.swift in Sources */,
+				DD9ECB742CA9A0C300AA7C45 /* RemoteControlConfig.swift in Sources */,
 				DDE179712C910127003CDDB7 /* OverrideStored+CoreDataProperties.swift in Sources */,
 				CD78BB94E43B249D60CC1A1B /* GlucoseNotificationSettingsRootView.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">
 <plist version="1.0">
 <dict>
+	<key>aps-environment</key>
+	<string>development</string>
 	<key>com.apple.developer.healthkit</key>
 	<true/>
 	<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">
 <plist version="1.0">
 <dict>
+	<key>TeamID</key>
+	<string>$(DEVELOPER_TEAM)</string>
 	<key>AppGroupID</key>
 	<string>$(APP_GROUP_ID)</string>
 	<key>BGTaskSchedulerPermittedIdentifiers</key>

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

@@ -1,5 +1,6 @@
 import Combine
 import Foundation
+import HealthKit
 import LoopKit
 import LoopKitUI
 import SwiftDate
@@ -101,6 +102,14 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         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?) {
         // if changed, remove all calibrations
         if self.cgmGlucoseSourceType != cgmGlucoseSourceType || self.cgmGlucosePluginId != cgmGlucosePluginId {
@@ -123,6 +132,8 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
             removeCalibrations()
         } else if self.cgmGlucoseSourceType == .plugin, cgmManager == nil, let rawCGMManager = rawCGMManager {
             cgmManager = cgmManagerFromRawValue(rawCGMManager)
+            updateManagerUnits(cgmManager)
+
         } else {
             saveConfigManager()
         }
@@ -152,7 +163,6 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         else {
             return nil
         }
-
         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 uploadedSettings = "upload/uploaded-settings.json"
         static let uploadedManualGlucose = "upload/uploaded-manual-readings.json"
+        static let uploadedNotes = "upload/uploaded-notes.json"
     }
 
     enum FreeAPS {

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

@@ -1,6 +1,28 @@
 import Foundation
 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 {
     private let processQueue = DispatchQueue(label: "DispatchQueue.JavaScriptWorker", attributes: .concurrent)
     private let virtualMachine: JSVirtualMachine
@@ -22,8 +44,16 @@ final class JavaScriptWorker {
                 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)
         return context
@@ -42,8 +72,41 @@ final class JavaScriptWorker {
         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! {
-        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! {
@@ -63,15 +126,21 @@ final class JavaScriptWorker {
 
     func inCommonContext<Value>(execute: (JavaScriptWorker) -> Value) -> Value {
         let context = getContext()
-        defer { returnContext(context) }
+        defer {
+            returnContext(context)
+        }
         return execute(self)
     }
 
     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
-            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

@@ -939,11 +939,11 @@ final class OpenAPS {
 
     private func middlewareScript(name: String) -> Script? {
         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: "") {
-            return Script(name: "Middleware", body: try! String(contentsOf: url))
+            return Script(name: name, body: try! String(contentsOf: url))
         }
 
         return nil

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

@@ -12,6 +12,7 @@ protocol OverrideStorage {
     func deleteOverridePreset(_ objectID: NSManagedObjectID) async
     func getOverridesNotYetUploadedToNightscout() async -> [NightscoutExercise]
     func getOverrideRunsNotYetUploadedToNightscout() async -> [NightscoutExercise]
+    func getPresetOverridesForNightscout() async -> [NightscoutPresetOverride]
 }
 
 final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
@@ -260,4 +261,31 @@ final class BaseOverrideStorage: @preconcurrency 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 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 apsManager = Logger(category: .apsManager, reporter: baseReporter)
     static let nightscout = Logger(category: .nightscout, reporter: baseReporter)
+    static let remoteControl = Logger(category: .remoteControl, reporter: baseReporter)
 
     enum Category: String {
         case `default`
@@ -121,6 +122,7 @@ final class Logger {
         case deviceManager
         case apsManager
         case nightscout
+        case remoteControl
 
         var name: String {
             rawValue.capitalizingFirstLetter()
@@ -135,6 +137,7 @@ final class Logger {
             case .deviceManager: return .deviceManager
             case .apsManager: return .apsManager
             case .nightscout: return .nightscout
+            case .remoteControl: return .remoteControl
             }
         }
 
@@ -147,6 +150,7 @@ final class Logger {
                  .deviceManager,
                  .nightscout,
                  .openAPS,
+                 .remoteControl,
                  .service:
                 return OSLog(subsystem: subsystem, category: name)
             }

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

@@ -52,4 +52,16 @@ struct NightscoutProfileStore: JSON {
     let units: String
     let enteredBy: String
     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 nsSensorChange = "Sensor Start"
     case capillaryGlucose = "BG Check"
+    case note = "Note"
 }
 
 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 predictionsForChart: Predictions?
         var simulatedDetermination: Determination?
-        var determinationObjectIDs: [NSManagedObjectID] = []
+        @MainActor var determinationObjectIDs: [NSManagedObjectID] = []
 
         var minForecast: [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 isSmoothingEnabled: Bool = false
         var stops: [Gradient.Stop] = []
@@ -736,7 +736,7 @@ extension Bolus.StateModel {
         if let forecastData = forecastData {
             simulatedDetermination = forecastData
         } else {
-            simulatedDetermination = await Task.detached { [self] in
+            simulatedDetermination = await Task { [self] in
                 await apsManager.simulateDetermineBasal(carbs: carbs, iob: amount)
             }.value
         }
@@ -761,14 +761,14 @@ extension Bolus.StateModel {
         minCount = max(12, nonEmptyArrays.map(\.count).min() ?? 0)
         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
             }
         }.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
             }
         }.value

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

@@ -67,6 +67,22 @@ extension NightscoutConfig {
             importedInsulinActionCurve = pumpSettings.insulinActionCurve
 
             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() {

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

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

+ 5 - 4
FreeAPS/Sources/Modules/OverrideConfig/View/AddOverrideForm.swift

@@ -3,7 +3,11 @@ import SwiftUI
 
 struct AddOverrideForm: View {
     @Environment(\.presentationMode) var presentationMode
-    @StateObject var state: OverrideConfig.StateModel
+    @Environment(\.colorScheme) var colorScheme
+    @Environment(\.dismiss) var dismiss
+
+    @Bindable var state: OverrideConfig.StateModel
+
     @State private var selectedIsfCrOption: IsfAndOrCrOptions = .isfAndCr
     @State private var selectedDisableSmbOption: DisableSmbOptions = .dontDisable
     @State private var percentageStep: Int = 5
@@ -17,9 +21,6 @@ struct AddOverrideForm: View {
     @State private var durationMinutes = 0
     @State private var overrideTarget = false
     @State private var didPressSave = false
-    @Environment(\.colorScheme) var colorScheme
-
-    @Environment(\.dismiss) var dismiss
 
     var color: LinearGradient {
         colorScheme == .dark ? LinearGradient(

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

@@ -2,7 +2,7 @@ import Foundation
 import SwiftUI
 
 struct EditOverrideForm: View {
-    @ObservedObject var override: OverrideStored
+    var override: OverrideStored
     @Environment(\.presentationMode) var presentationMode
     @Environment(\.colorScheme) var colorScheme
     @Bindable var state: OverrideConfig.StateModel
@@ -24,7 +24,6 @@ struct EditOverrideForm: View {
     @State private var uamMinutes: Decimal?
     @State private var selectedIsfCrOption: IsfAndOrCrOptions
     @State private var selectedDisableSmbOption: DisableSmbOptions
-
     @State private var hasChanges = false
     @State private var isEditing = false
     @State private var target_override = false
@@ -549,7 +548,9 @@ struct EditOverrideForm: View {
                         guard let moc = override.managedObjectContext else { return }
                         guard moc.hasChanges else { return }
                         try moc.save()
-
+                        Task {
+                            await state.nightscoutManager.uploadProfiles()
+                        }
                         // Disable previous active Override
                         if let currentActiveOverride = state.currentActiveOverride {
                             Task {

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

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

@@ -0,0 +1,53 @@
+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,
+            isPreset: false,
+            enabled: true,
+            halfBasalTarget: settings.preferences.halfBasalExerciseTarget
+        )
+
+        // TODO: this should probably be try-catch'd ?
+        await tempTargetsStorage.storeTempTarget(tempTarget: 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())
+        await tempTargetsStorage.storeTempTarget(tempTarget: 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"]
         ),
         SettingItem(
+            title: "Remote Control",
+            view: .remoteControlConfig,
+            searchContents: ["Remote Control"],
+            path: ["Features", "Remote Control"]
+        ),
+        SettingItem(
             title: "User Interface",
             view: .userInterfaceSettings,
             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("Meal Settings").navigationLink(to: .mealSettings, from: self)
                     Text("Shortcuts").navigationLink(to: .shortcutsConfig, from: self)
+                    Text("Remote Control").navigationLink(to: .remoteControlConfig, from: self)
                 }
             )
             .listRowBackground(Color.chart)

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

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

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

@@ -19,6 +19,7 @@ protocol NightscoutManager: GlucoseSource {
     func uploadProfiles() async
     func importSettings() async -> ScheduledNightscoutProfile?
     var cgmURL: URL? { get }
+    func uploadNoteTreatment(note: String) async
 }
 
 final class BaseNightscoutManager: NightscoutManager, Injectable {
@@ -36,6 +37,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     @Injected() private var reachabilityManager: ReachabilityManager!
     @Injected() var healthkitManager: HealthKitManager!
 
+    private let uploadOverridesSubject = PassthroughSubject<Void, Never>()
     private let processQueue = DispatchQueue(label: "BaseNetworkManager.processQueue")
     private var ping: TimeInterval?
 
@@ -95,6 +97,16 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             }
             .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()
         setupNotification()
     }
@@ -116,17 +128,11 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         }.store(in: &subscriptions)
 
         coreDataPublisher?.filterByEntityName("OverrideStored").sink { [weak self] _ in
-            guard let self = self else { return }
-            Task.detached {
-                await self.uploadOverrides()
-            }
+            self?.uploadOverridesSubject.send()
         }.store(in: &subscriptions)
 
         coreDataPublisher?.filterByEntityName("OverrideRunStored").sink { [weak self] _ in
-            guard let self = self else { return }
-            Task.detached {
-                await self.uploadOverrides()
-            }
+            self?.uploadOverridesSubject.send()
         }.store(in: &subscriptions)
 
         coreDataPublisher?.filterByEntityName("PumpEventStored").sink { [weak self] _ in
@@ -619,13 +625,25 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                 let defaultProfile = "default"
 
                 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(
                     defaultProfile: defaultProfile,
                     startDate: now,
                     mills: Int(now.timeIntervalSince1970) * 1000,
                     units: nsUnits,
                     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 {
@@ -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 {

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

@@ -195,28 +195,30 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
 
     @MainActor private func configureState() async {
         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
         }
 
         do {
             let glucoseValues: [GlucoseStored] = await CoreDataStack.shared
                 .getNSManagedObject(with: glucoseValuesIds, context: viewContext)
-
             let lastDetermination = try viewContext.existingObject(with: lastDeterminationId) as? OrefDetermination
-            let latestOverride = try viewContext.existingObject(with: latestOverrideId) as? OverrideStored
-
             let recommendedInsulin = await newBolusCalc(
                 glucoseIds: glucoseValuesIds,
                 determinationId: lastDeterminationId
             )
 
+            var latestOverride: OverrideStored?
+            if let id = latestOverrideId {
+                latestOverride = try viewContext.existingObject(with: id) as? OverrideStored
+            }
+
             await MainActor.run { [weak self] in
                 guard let self = self else { return }
 
@@ -296,12 +298,13 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
 
                 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()

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

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

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

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

+ 1 - 1
G7SensorKit

@@ -1 +1 @@
-Subproject commit b5e992e211d2ac6224acb105dd97fb484767da72
+Subproject commit 2be3eb29b0a18aa89f8b60281341e46e07d024e5

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

@@ -19,7 +19,7 @@
         <attribute name="note" optional="YES" attributeType="String"/>
         <attribute name="protein" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
         <fetchIndex name="byDate">
-            <fetchIndexElement property="date" type="Binary" order="ascending"/>
+            <fetchIndexElement property="date" type="Binary" order="descending"/>
         </fetchIndex>
         <fetchIndex name="byIsFPU">
             <fetchIndexElement property="isFPU" type="Binary" order="ascending"/>

+ 1 - 1
OmniBLE

@@ -1 +1 @@
-Subproject commit e39834584548821adf442f13abed0d5cfd237a72
+Subproject commit 4ad811774c09cae208678552dbc20ee6cc9d4f59

+ 1 - 1
OmniKit

@@ -1 +1 @@
-Subproject commit 849dc7abc821728dae7e064176a409e6ceb0dadd
+Subproject commit 01bc59889b9216737942ea3f0cab22f6a6c4a0e8

+ 1 - 1
README.md

@@ -63,7 +63,7 @@ Instructions in greater detail, but not Trio-specific:
 
 # 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/)
 

+ 2 - 1
fastlane/Fastfile

@@ -187,7 +187,8 @@ platform :ios do
     configure_bundle_id("Trio", "#{BUNDLE_ID}", [
       Spaceship::ConnectAPI::BundleIdCapability::Type::APP_GROUPS,
       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", [