ソースを参照

Release 2.3.0 (#400)

* Import/Export profiles from Nightscout. Allows your endocrinologist, or you and your endo (preferably), to go through your settings in NS (ISF, CR, basals, DIA, targets). When updated in NS you can import all these setting to your iPhone iAPS app with a new import settings button in iAPS Nightscout settings. 

Can also be used when onboarding a new iAPS app. You can import all settings from Nightscout with just a tap. To import you first need to select a pump, but even a simulator pump works for import to begin. 

All profile settings will be imported and saved to pump. 

All your settings (yes every one of them) are uploaded uploaded automatically whenever changed (and only when changed). For the auto upload of updated settings to NS you need to have NS configured and upload toggled on in iAPS NS settings. 

* Replace Basal Schedules with Autotune Basals (new button). This feature has been requested many times. Now your normal basal profile will be replaced with the autotuned basal settings and saved to pump with a push of a new button in Autotune settings.
Jon B Mårtensson 2 年 前
コミット
40e3fad664

+ 4 - 0
.github/workflows/add_identifiers.yml

@@ -24,6 +24,10 @@ jobs:
       - name: Patch Match Tables
       - name: Patch Match Tables
         run: find /usr/local/lib/ruby/gems -name table_printer.rb | xargs sed -i "" "/puts(Terminal::Table.new(params))/d"
         run: find /usr/local/lib/ruby/gems -name table_printer.rb | xargs sed -i "" "/puts(Terminal::Table.new(params))/d"
         
         
+      # Sync the GitHub runner clock with the Windows time server (workaround as suggested in https://github.com/actions/runner/issues/2996)
+      - name: Sync clock
+        run: sudo sntp -sS time.windows.com
+
       # Create or update identifiers for app
       # Create or update identifiers for app
       - name: Fastlane Provision
       - name: Fastlane Provision
         run: fastlane identifiers
         run: fastlane identifiers

+ 4 - 0
.github/workflows/build_iAPS.yml

@@ -34,6 +34,10 @@ jobs:
       - name: Patch Match Tables
       - name: Patch Match Tables
         run: find /usr/local/lib/ruby/gems -name table_printer.rb | xargs sed -i "" "/puts(Terminal::Table.new(params))/d"
         run: find /usr/local/lib/ruby/gems -name table_printer.rb | xargs sed -i "" "/puts(Terminal::Table.new(params))/d"
       
       
+      # Sync the GitHub runner clock with the Windows time server (workaround as suggested in https://github.com/actions/runner/issues/2996)
+      - name: Sync clock
+        run: sudo sntp -sS time.windows.com
+
       # Build signed iAPS IPA file
       # Build signed iAPS IPA file
       - name: Fastlane Build & Archive
       - name: Fastlane Build & Archive
         run: fastlane build_iAPS
         run: fastlane build_iAPS

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

@@ -23,7 +23,11 @@ jobs:
       # Patch Fastlane Match to not print tables
       # Patch Fastlane Match to not print tables
       - name: Patch Match Tables
       - name: Patch Match Tables
         run: find /usr/local/lib/ruby/gems -name table_printer.rb | xargs sed -i "" "/puts(Terminal::Table.new(params))/d"
         run: find /usr/local/lib/ruby/gems -name table_printer.rb | xargs sed -i "" "/puts(Terminal::Table.new(params))/d"
-        
+
+      # Sync the GitHub runner clock with the Windows time server (workaround as suggested in https://github.com/actions/runner/issues/2996)
+      - name: Sync clock
+        run: sudo sntp -sS time.windows.com
+
       # Create or update certificates for app
       # Create or update certificates for app
       - name: Create Certificates
       - name: Create Certificates
         run: fastlane certs
         run: fastlane certs

+ 4 - 0
.github/workflows/validate_secrets.yml

@@ -10,6 +10,10 @@ jobs:
       - name: Checkout Repo
       - name: Checkout Repo
         uses: actions/checkout@v3
         uses: actions/checkout@v3
 
 
+      # Sync the GitHub runner clock with the Windows time server (workaround as suggested in https://github.com/actions/runner/issues/2996)
+      - name: Sync clock
+        run: sudo sntp -sS time.windows.com
+
       # Validates the repo secrets
       # Validates the repo secrets
       - name: Validate Secrets
       - name: Validate Secrets
         run: |
         run: |

+ 1 - 1
Config.xcconfig

@@ -1,5 +1,5 @@
 APP_DISPLAY_NAME = iAPS
 APP_DISPLAY_NAME = iAPS
-APP_VERSION = 2.2.8
+APP_VERSION = 2.3.0
 APP_BUILD_NUMBER = 1
 APP_BUILD_NUMBER = 1
 COPYRIGHT_NOTICE =
 COPYRIGHT_NOTICE =
 DEVELOPER_TEAM = ##TEAM_ID##
 DEVELOPER_TEAM = ##TEAM_ID##

+ 4 - 0
Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents

@@ -31,6 +31,10 @@
         <attribute name="hba1c_7" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
         <attribute name="hba1c_7" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
         <attribute name="hba1c_30" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
         <attribute name="hba1c_30" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
     </entity>
     </entity>
+    <entity name="ImportError" representedClassName="ImportError" syncable="YES" codeGenerationType="class">
+        <attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
+        <attribute name="error" optional="YES" attributeType="String"/>
+    </entity>
     <entity name="InsulinDistribution" representedClassName="InsulinDistribution" syncable="YES" codeGenerationType="class">
     <entity name="InsulinDistribution" representedClassName="InsulinDistribution" syncable="YES" codeGenerationType="class">
         <attribute name="bolus" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
         <attribute name="bolus" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
         <attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
         <attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>

+ 8 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -18,6 +18,7 @@
 		190EBCC629FF138000BA767D /* StatConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 190EBCC529FF138000BA767D /* StatConfigProvider.swift */; };
 		190EBCC629FF138000BA767D /* StatConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 190EBCC529FF138000BA767D /* StatConfigProvider.swift */; };
 		190EBCC829FF13AA00BA767D /* StatConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 190EBCC729FF13AA00BA767D /* StatConfigStateModel.swift */; };
 		190EBCC829FF13AA00BA767D /* StatConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 190EBCC729FF13AA00BA767D /* StatConfigStateModel.swift */; };
 		190EBCCB29FF13CB00BA767D /* StatConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 190EBCCA29FF13CB00BA767D /* StatConfigRootView.swift */; };
 		190EBCCB29FF13CB00BA767D /* StatConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 190EBCCA29FF13CB00BA767D /* StatConfigRootView.swift */; };
+		191F62682AD6B05A004D7911 /* NightscoutSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 191F62672AD6B05A004D7911 /* NightscoutSettings.swift */; };
 		1927C8E62744606D00347C69 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1927C8E82744606D00347C69 /* InfoPlist.strings */; };
 		1927C8E62744606D00347C69 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1927C8E82744606D00347C69 /* InfoPlist.strings */; };
 		1935364028496F7D001E0B16 /* Oref2_variables.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1935363F28496F7D001E0B16 /* Oref2_variables.swift */; };
 		1935364028496F7D001E0B16 /* Oref2_variables.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1935363F28496F7D001E0B16 /* Oref2_variables.swift */; };
 		193F6CDD2A512C8F001240FD /* Loops.swift in Sources */ = {isa = PBXBuildFile; fileRef = 193F6CDC2A512C8F001240FD /* Loops.swift */; };
 		193F6CDD2A512C8F001240FD /* Loops.swift in Sources */ = {isa = PBXBuildFile; fileRef = 193F6CDC2A512C8F001240FD /* Loops.swift */; };
@@ -303,6 +304,7 @@
 		BF1667ADE69E4B5B111CECAE /* ManualTempBasalProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680C4420C9A345D46D90D06C /* ManualTempBasalProvider.swift */; };
 		BF1667ADE69E4B5B111CECAE /* ManualTempBasalProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680C4420C9A345D46D90D06C /* ManualTempBasalProvider.swift */; };
 		C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFCFE0781F9074C2917890E8 /* ManualTempBasalStateModel.swift */; };
 		C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFCFE0781F9074C2917890E8 /* ManualTempBasalStateModel.swift */; };
 		CA370FC152BC98B3D1832968 /* BasalProfileEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */; };
 		CA370FC152BC98B3D1832968 /* BasalProfileEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */; };
+		CC6C406E2ACDD69E009B8058 /* RawFetchedProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC6C406D2ACDD69E009B8058 /* RawFetchedProfile.swift */; };
 		CD78BB94E43B249D60CC1A1B /* NotificationsConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22963BD06A9C83959D4914E4 /* NotificationsConfigRootView.swift */; };
 		CD78BB94E43B249D60CC1A1B /* NotificationsConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22963BD06A9C83959D4914E4 /* NotificationsConfigRootView.swift */; };
 		CE2FAD38297D69E1001A872C /* ShareClient.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CE398D1A297D69A900DF218F /* ShareClient.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		CE2FAD38297D69E1001A872C /* ShareClient.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CE398D1A297D69A900DF218F /* ShareClient.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		CE2FAD3A297D93F0001A872C /* BloodGlucoseExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2FAD39297D93F0001A872C /* BloodGlucoseExtensions.swift */; };
 		CE2FAD3A297D93F0001A872C /* BloodGlucoseExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2FAD39297D93F0001A872C /* BloodGlucoseExtensions.swift */; };
@@ -492,6 +494,7 @@
 		190EBCC729FF13AA00BA767D /* StatConfigStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatConfigStateModel.swift; sourceTree = "<group>"; };
 		190EBCC729FF13AA00BA767D /* StatConfigStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatConfigStateModel.swift; sourceTree = "<group>"; };
 		190EBCCA29FF13CB00BA767D /* StatConfigRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatConfigRootView.swift; sourceTree = "<group>"; };
 		190EBCCA29FF13CB00BA767D /* StatConfigRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatConfigRootView.swift; sourceTree = "<group>"; };
 		1918333A26ADA46800F45722 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = "<group>"; };
 		1918333A26ADA46800F45722 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = "<group>"; };
+		191F62672AD6B05A004D7911 /* NightscoutSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutSettings.swift; sourceTree = "<group>"; };
 		1927C8E92744611700347C69 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/InfoPlist.strings; sourceTree = "<group>"; };
 		1927C8E92744611700347C69 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/InfoPlist.strings; sourceTree = "<group>"; };
 		1927C8EA2744611800347C69 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/InfoPlist.strings; sourceTree = "<group>"; };
 		1927C8EA2744611800347C69 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/InfoPlist.strings; sourceTree = "<group>"; };
 		1927C8EB2744611900347C69 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
 		1927C8EB2744611900347C69 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
@@ -818,6 +821,7 @@
 		C19984D62EFC0035A9E9644D /* BolusProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BolusProvider.swift; sourceTree = "<group>"; };
 		C19984D62EFC0035A9E9644D /* BolusProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BolusProvider.swift; sourceTree = "<group>"; };
 		C377490C77661D75E8C50649 /* ManualTempBasalRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalRootView.swift; sourceTree = "<group>"; };
 		C377490C77661D75E8C50649 /* ManualTempBasalRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalRootView.swift; sourceTree = "<group>"; };
 		C8D1A7CA8C10C4403D4BBFA7 /* BolusDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BolusDataFlow.swift; sourceTree = "<group>"; };
 		C8D1A7CA8C10C4403D4BBFA7 /* BolusDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BolusDataFlow.swift; sourceTree = "<group>"; };
+		CC6C406D2ACDD69E009B8058 /* RawFetchedProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RawFetchedProfile.swift; sourceTree = "<group>"; };
 		CE2FAD39297D93F0001A872C /* BloodGlucoseExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloodGlucoseExtensions.swift; sourceTree = "<group>"; };
 		CE2FAD39297D93F0001A872C /* BloodGlucoseExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloodGlucoseExtensions.swift; sourceTree = "<group>"; };
 		CE398D012977349800DF218F /* CryptoKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CryptoKit.framework; path = System/Library/Frameworks/CryptoKit.framework; sourceTree = SDKROOT; };
 		CE398D012977349800DF218F /* CryptoKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CryptoKit.framework; path = System/Library/Frameworks/CryptoKit.framework; sourceTree = SDKROOT; };
 		CE398D15297C9D1D00DF218F /* dexcomSourceG7.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dexcomSourceG7.swift; sourceTree = "<group>"; };
 		CE398D15297C9D1D00DF218F /* dexcomSourceG7.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dexcomSourceG7.swift; sourceTree = "<group>"; };
@@ -1602,10 +1606,12 @@
 				19012CDB291D2CB900FB8210 /* LoopStats.swift */,
 				19012CDB291D2CB900FB8210 /* LoopStats.swift */,
 				FE41E4D329463C660047FD55 /* NightscoutStatistics.swift */,
 				FE41E4D329463C660047FD55 /* NightscoutStatistics.swift */,
 				FE41E4D529463EE20047FD55 /* NightscoutPreferences.swift */,
 				FE41E4D529463EE20047FD55 /* NightscoutPreferences.swift */,
+				191F62672AD6B05A004D7911 /* NightscoutSettings.swift */,
 				1967DFBD29D052C200759F30 /* Icons.swift */,
 				1967DFBD29D052C200759F30 /* Icons.swift */,
 				19D4E4EA29FC6A9F00351451 /* TIRforChart.swift */,
 				19D4E4EA29FC6A9F00351451 /* TIRforChart.swift */,
 				19A910352A24D6D700C8951B /* DateFilter.swift */,
 				19A910352A24D6D700C8951B /* DateFilter.swift */,
 				193F6CDC2A512C8F001240FD /* Loops.swift */,
 				193F6CDC2A512C8F001240FD /* Loops.swift */,
+				CC6C406D2ACDD69E009B8058 /* RawFetchedProfile.swift */,
 			);
 			);
 			path = Models;
 			path = Models;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -2619,6 +2625,7 @@
 				38A9260525F012D8009E3739 /* CarbRatios.swift in Sources */,
 				38A9260525F012D8009E3739 /* CarbRatios.swift in Sources */,
 				38FCF3D625E8FDF40078B0D1 /* MD5.swift in Sources */,
 				38FCF3D625E8FDF40078B0D1 /* MD5.swift in Sources */,
 				3871F39C25ED892B0013ECB5 /* TempTarget.swift in Sources */,
 				3871F39C25ED892B0013ECB5 /* TempTarget.swift in Sources */,
+				191F62682AD6B05A004D7911 /* NightscoutSettings.swift in Sources */,
 				FEFA5C11299F814A00765C17 /* CoreDataStack.swift in Sources */,
 				FEFA5C11299F814A00765C17 /* CoreDataStack.swift in Sources */,
 				3811DEAB25C9D88300A708ED /* HTTPResponseStatus.swift in Sources */,
 				3811DEAB25C9D88300A708ED /* HTTPResponseStatus.swift in Sources */,
 				3811DE5F25C9D4D500A708ED /* ProgressBar.swift in Sources */,
 				3811DE5F25C9D4D500A708ED /* ProgressBar.swift in Sources */,
@@ -2693,6 +2700,7 @@
 				63E890B4D951EAA91C071D5C /* BasalProfileEditorStateModel.swift in Sources */,
 				63E890B4D951EAA91C071D5C /* BasalProfileEditorStateModel.swift in Sources */,
 				CE398D16297C9D1D00DF218F /* dexcomSourceG7.swift in Sources */,
 				CE398D16297C9D1D00DF218F /* dexcomSourceG7.swift in Sources */,
 				38FEF3FA2737E42000574A46 /* BaseStateModel.swift in Sources */,
 				38FEF3FA2737E42000574A46 /* BaseStateModel.swift in Sources */,
+				CC6C406E2ACDD69E009B8058 /* RawFetchedProfile.swift in Sources */,
 				385CEA8225F23DFD002D6D5B /* NightscoutStatus.swift in Sources */,
 				385CEA8225F23DFD002D6D5B /* NightscoutStatus.swift in Sources */,
 				F90692AA274B7AAE0037068D /* HealthKitManager.swift in Sources */,
 				F90692AA274B7AAE0037068D /* HealthKitManager.swift in Sources */,
 				38887CCE25F5725200944304 /* IOBEntry.swift in Sources */,
 				38887CCE25F5725200944304 /* IOBEntry.swift in Sources */,

+ 4 - 4
FreeAPS/Resources/Assets.xcassets/Colors/Basal.colorset/Contents.json

@@ -22,10 +22,10 @@
       "color" : {
       "color" : {
         "color-space" : "srgb",
         "color-space" : "srgb",
         "components" : {
         "components" : {
-          "alpha" : "1.000",
-          "blue" : "1.000",
-          "green" : "1.000",
-          "red" : "1.000"
+          "alpha" : "0.500",
+          "blue" : "0.988",
+          "green" : "0.588",
+          "red" : "0.118"
         }
         }
       },
       },
       "idiom" : "universal"
       "idiom" : "universal"

+ 0 - 6
FreeAPS/Sources/APS/APSManager.swift

@@ -1192,19 +1192,15 @@ final class BaseAPSManager: APSManager, Injectable {
                 )
                 )
                 storage.save(dailystat, as: file)
                 storage.save(dailystat, as: file)
                 nightscout.uploadStatistics(dailystat: dailystat)
                 nightscout.uploadStatistics(dailystat: dailystat)
-                nightscout.uploadPreferences()
 
 
                 let saveStatsCoreData = StatsData(context: self.coredataContext)
                 let saveStatsCoreData = StatsData(context: self.coredataContext)
                 saveStatsCoreData.lastrun = Date()
                 saveStatsCoreData.lastrun = Date()
                 try? self.coredataContext.save()
                 try? self.coredataContext.save()
-                print("Test time of statistics computation: \(-1 * now.timeIntervalSinceNow) s")
             }
             }
         }
         }
     }
     }
 
 
     private func loopStats(loopStatRecord: LoopStats) {
     private func loopStats(loopStatRecord: LoopStats) {
-        let LoopStatsStartedAt = Date()
-
         coredataContext.perform {
         coredataContext.perform {
             let nLS = LoopStatRecord(context: self.coredataContext)
             let nLS = LoopStatRecord(context: self.coredataContext)
 
 
@@ -1216,8 +1212,6 @@ final class BaseAPSManager: APSManager, Injectable {
 
 
             try? self.coredataContext.save()
             try? self.coredataContext.save()
         }
         }
-        print("LoopStatRecords: \(loopStatRecord)")
-        print("Test time of LoopStats computation: \(-1 * LoopStatsStartedAt.timeIntervalSinceNow) s")
     }
     }
 
 
     private func processError(_ error: Error) {
     private func processError(_ error: Error) {

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

@@ -54,7 +54,6 @@ extension OpenAPS {
         static let iob = "monitor/iob.json"
         static let iob = "monitor/iob.json"
         static let cgmState = "monitor/cgm-state.json"
         static let cgmState = "monitor/cgm-state.json"
         static let podAge = "monitor/pod-age.json"
         static let podAge = "monitor/pod-age.json"
-        // static let tdd = "monitor/tdd.json"
         static let oref2_variables = "monitor/oref2_variables.json"
         static let oref2_variables = "monitor/oref2_variables.json"
         static let alertHistory = "monitor/alerthistory.json"
         static let alertHistory = "monitor/alerthistory.json"
         static let statistics = "monitor/statistics.json"
         static let statistics = "monitor/statistics.json"
@@ -86,6 +85,8 @@ extension OpenAPS {
         static let uploadedCGMState = "upload/uploaded-cgm-state.json"
         static let uploadedCGMState = "upload/uploaded-cgm-state.json"
         static let uploadedPodAge = "upload/uploaded-pod-age.json"
         static let uploadedPodAge = "upload/uploaded-pod-age.json"
         static let uploadedProfile = "upload/uploaded-profile.json"
         static let uploadedProfile = "upload/uploaded-profile.json"
+        static let uploadedPreferences = "upload/uploaded-preferences.json"
+        static let uploadedSettings = "upload/uploaded-settings.json"
     }
     }
 
 
     enum FreeAPS {
     enum FreeAPS {

+ 24 - 0
FreeAPS/Sources/Models/FetchedProfile.swift

@@ -0,0 +1,24 @@
+import Foundation
+
+struct FetchedNightscoutProfileStore: JSON {
+    let _id: String
+    let defaultProfile: String
+    let startDate: String
+    let mills: Decimal
+    let enteredBy: String
+    let store: [String: ScheduledNightscoutProfile]
+    let created_at: String
+}
+
+struct FetchedNightscoutProfile: JSON {
+    let dia: Decimal
+    let carbs_hr: Int
+    let delay: Decimal
+    let timezone: String
+    let target_low: [NightscoutTimevalue]
+    let target_high: [NightscoutTimevalue]
+    let sens: [NightscoutTimevalue]
+    let basal: [NightscoutTimevalue]
+    let carbratio: [NightscoutTimevalue]
+    let units: String
+}

+ 1 - 1
FreeAPS/Sources/Models/NightscoutPreferences.swift

@@ -1,6 +1,6 @@
 import Foundation
 import Foundation
 
 
 struct NightscoutPreferences: JSON {
 struct NightscoutPreferences: JSON {
-    let report = "preferences"
+    var report = "preferences"
     let preferences: Preferences?
     let preferences: Preferences?
 }
 }

+ 6 - 0
FreeAPS/Sources/Models/NightscoutSettings.swift

@@ -0,0 +1,6 @@
+import Foundation
+
+struct NightscoutSettings: JSON {
+    let report = "settings"
+    let settings: FreeAPSSettings?
+}

+ 1 - 1
FreeAPS/Sources/Models/NightscoutStatus.swift

@@ -29,7 +29,7 @@ struct Uploader: JSON {
 struct NightscoutTimevalue: JSON {
 struct NightscoutTimevalue: JSON {
     let time: String
     let time: String
     let value: Decimal
     let value: Decimal
-    let timeAsSeconds: Int
+    let timeAsSeconds: Int?
 }
 }
 
 
 struct ScheduledNightscoutProfile: JSON {
 struct ScheduledNightscoutProfile: JSON {

+ 24 - 0
FreeAPS/Sources/Models/RawFetchedProfile.swift

@@ -0,0 +1,24 @@
+import Foundation
+
+struct FetchedNightscoutProfileStore: JSON {
+    let _id: String
+    let defaultProfile: String
+    let startDate: String
+    let mills: Decimal
+    let enteredBy: String
+    let store: [String: ScheduledNightscoutProfile]
+    let created_at: String
+}
+
+struct FetchedNightscoutProfile: JSON {
+    let dia: Decimal
+    let carbs_hr: Int
+    let delay: Decimal
+    let timezone: String
+    let target_low: [NightscoutTimevalue]
+    let target_high: [NightscoutTimevalue]
+    let sens: [NightscoutTimevalue]
+    let basal: [NightscoutTimevalue]
+    let carbratio: [NightscoutTimevalue]
+    let units: String
+}

+ 32 - 0
FreeAPS/Sources/Modules/AutotuneConfig/AutotuneConfigStateModel.swift

@@ -1,9 +1,11 @@
 import Combine
 import Combine
+import LoopKit
 import SwiftUI
 import SwiftUI
 
 
 extension AutotuneConfig {
 extension AutotuneConfig {
     final class StateModel: BaseStateModel<Provider> {
     final class StateModel: BaseStateModel<Provider> {
         @Injected() var apsManager: APSManager!
         @Injected() var apsManager: APSManager!
+        @Injected() private var storage: FileStorage!
         @Published var useAutotune = false
         @Published var useAutotune = false
         @Published var onlyAutotuneBasals = false
         @Published var onlyAutotuneBasals = false
         @Published var autotune: Autotune?
         @Published var autotune: Autotune?
@@ -59,5 +61,35 @@ extension AutotuneConfig {
                 .cancellable()
                 .cancellable()
                 .store(in: &lifetime)
                 .store(in: &lifetime)
         }
         }
+
+        func replace() {
+            if let autotunedBasals = autotune {
+                let basals = autotunedBasals.basalProfile
+                    .map { basal -> BasalProfileEntry in
+                        BasalProfileEntry(
+                            start: String(basal.start.prefix(5)),
+                            minutes: basal.minutes,
+                            rate: basal.rate
+                        )
+                    }
+                guard let pump = apsManager.pumpManager else {
+                    storage.save(basals, as: OpenAPS.Settings.basalProfile)
+                    debug(.service, "Basals have been replaced with Autotuned Basals by user.")
+                    return
+                }
+                let syncValues = basals.map {
+                    RepeatingScheduleValue(startTime: TimeInterval($0.minutes * 60), value: Double($0.rate))
+                }
+                pump.syncBasalRateSchedule(items: syncValues) { result in
+                    switch result {
+                    case .success:
+                        self.storage.save(basals, as: OpenAPS.Settings.basalProfile)
+                        debug(.service, "Basals saved to pump!")
+                    case .failure:
+                        debug(.service, "Basals couldn't be save to pump")
+                    }
+                }
+            }
+        }
     }
     }
 }
 }

+ 17 - 0
FreeAPS/Sources/Modules/AutotuneConfig/View/AutotuneConfigRootView.swift

@@ -5,6 +5,7 @@ extension AutotuneConfig {
     struct RootView: BaseView {
     struct RootView: BaseView {
         let resolver: Resolver
         let resolver: Resolver
         @StateObject var state = StateModel()
         @StateObject var state = StateModel()
+        @State var replaceAlert = false
 
 
         private var isfFormatter: NumberFormatter {
         private var isfFormatter: NumberFormatter {
             let formatter = NumberFormatter()
             let formatter = NumberFormatter()
@@ -94,11 +95,27 @@ extension AutotuneConfig {
                         label: { Text("Delete autotune data") }
                         label: { Text("Delete autotune data") }
                             .foregroundColor(.red)
                             .foregroundColor(.red)
                     }
                     }
+
+                    Section {
+                        Button {
+                            replaceAlert = true
+                        }
+                        label: { Text("Save as your Normal Basal Rates") }
+                    } header: {
+                        Text("Replace Normal Basal")
+                    }
                 }
                 }
             }
             }
             .onAppear(perform: configureView)
             .onAppear(perform: configureView)
             .navigationTitle("Autotune")
             .navigationTitle("Autotune")
             .navigationBarTitleDisplayMode(.automatic)
             .navigationBarTitleDisplayMode(.automatic)
+            .alert(Text("Are you sure?"), isPresented: $replaceAlert) {
+                Button("Yes", action: {
+                    state.replace()
+                    replaceAlert.toggle()
+                })
+                Button("No", action: { replaceAlert.toggle() })
+            }
         }
         }
     }
     }
 }
 }

+ 250 - 4
FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift

@@ -1,6 +1,8 @@
 import CGMBLEKit
 import CGMBLEKit
 import Combine
 import Combine
+import CoreData
 import G7SensorKit
 import G7SensorKit
+import LoopKit
 import SwiftDate
 import SwiftDate
 import SwiftUI
 import SwiftUI
 
 
@@ -11,6 +13,10 @@ extension NightscoutConfig {
         @Injected() private var glucoseStorage: GlucoseStorage!
         @Injected() private var glucoseStorage: GlucoseStorage!
         @Injected() private var healthKitManager: HealthKitManager!
         @Injected() private var healthKitManager: HealthKitManager!
         @Injected() private var cgmManager: FetchGlucoseManager!
         @Injected() private var cgmManager: FetchGlucoseManager!
+        @Injected() private var storage: FileStorage!
+        @Injected() var apsManager: APSManager!
+
+        let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
 
 
         @Published var url = ""
         @Published var url = ""
         @Published var secret = ""
         @Published var secret = ""
@@ -22,11 +28,21 @@ extension NightscoutConfig {
         @Published var uploadGlucose = true // Upload Glucose
         @Published var uploadGlucose = true // Upload Glucose
         @Published var useLocalSource = false
         @Published var useLocalSource = false
         @Published var localPort: Decimal = 0
         @Published var localPort: Decimal = 0
+        @Published var units: GlucoseUnits = .mmolL
+        @Published var dia: Decimal = 6
+        @Published var maxBasal: Decimal = 2
+        @Published var maxBolus: Decimal = 10
+        @Published var allowAnnouncements: Bool = false
 
 
         override func subscribe() {
         override func subscribe() {
             url = keychain.getValue(String.self, forKey: Config.urlKey) ?? ""
             url = keychain.getValue(String.self, forKey: Config.urlKey) ?? ""
             secret = keychain.getValue(String.self, forKey: Config.secretKey) ?? ""
             secret = keychain.getValue(String.self, forKey: Config.secretKey) ?? ""
+            units = settingsManager.settings.units
+            dia = settingsManager.pumpSettings.insulinActionCurve
+            maxBasal = settingsManager.pumpSettings.maxBasal
+            maxBolus = settingsManager.pumpSettings.maxBolus
 
 
+            subscribeSetting(\.allowAnnouncements, on: $allowAnnouncements) { allowAnnouncements = $0 }
             subscribeSetting(\.isUploadEnabled, on: $isUploadEnabled) { isUploadEnabled = $0 }
             subscribeSetting(\.isUploadEnabled, on: $isUploadEnabled) { isUploadEnabled = $0 }
             subscribeSetting(\.useLocalGlucoseSource, on: $useLocalSource) { useLocalSource = $0 }
             subscribeSetting(\.useLocalGlucoseSource, on: $useLocalSource) { useLocalSource = $0 }
             subscribeSetting(\.localGlucosePort, on: $localPort.map(Int.init)) { localPort = Decimal($0) }
             subscribeSetting(\.localGlucosePort, on: $localPort.map(Int.init)) { localPort = Decimal($0) }
@@ -45,10 +61,6 @@ extension NightscoutConfig {
         }
         }
 
 
         func connect() {
         func connect() {
-            if let CheckURL = url.last, CheckURL == "/" {
-                let fixedURL = url.dropLast()
-                url = String(fixedURL)
-            }
             guard let url = URL(string: url) else {
             guard let url = URL(string: url) else {
                 message = "Invalid URL"
                 message = "Invalid URL"
                 return
                 return
@@ -72,6 +84,240 @@ extension NightscoutConfig {
                 .store(in: &lifetime)
                 .store(in: &lifetime)
         }
         }
 
 
+        private var nightscoutAPI: NightscoutAPI? {
+            guard let urlString = keychain.getValue(String.self, forKey: NightscoutConfig.Config.urlKey),
+                  let url = URL(string: urlString),
+                  let secret = keychain.getValue(String.self, forKey: NightscoutConfig.Config.secretKey)
+            else {
+                return nil
+            }
+            return NightscoutAPI(url: url, secret: secret)
+        }
+
+        func importSettings() {
+            guard let nightscout = nightscoutAPI else {
+                saveError("Can't access nightscoutAPI")
+                return
+            }
+            let group = DispatchGroup()
+            group.enter()
+            var error = ""
+            let path = "/api/v1/profile.json"
+            let timeout: TimeInterval = 60
+
+            var components = URLComponents()
+            components.scheme = nightscout.url.scheme
+            components.host = nightscout.url.host
+            components.port = nightscout.url.port
+            components.path = path
+            components.queryItems = [
+                URLQueryItem(name: "count", value: "1")
+            ]
+            var url = URLRequest(url: components.url!)
+            url.allowsConstrainedNetworkAccess = false
+            url.timeoutInterval = timeout
+
+            if let secret = nightscout.secret {
+                url.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
+            }
+            let task = URLSession.shared.dataTask(with: url) { data, response, error_ in
+                if let error_ = error_ {
+                    print("Error occured: " + error_.localizedDescription)
+                    // handle error
+                    self.saveError("Error occured: " + error_.localizedDescription)
+                    error = error_.localizedDescription
+                    return
+                }
+                guard let httpResponse = response as? HTTPURLResponse,
+                      (200 ... 299).contains(httpResponse.statusCode)
+                else {
+                    print("Error occured! " + error_.debugDescription)
+                    // handle error
+                    self.saveError(error_.debugDescription)
+                    return
+                }
+                let jsonDecoder = JSONCoding.decoder
+
+                if let mimeType = httpResponse.mimeType, mimeType == "application/json",
+                   let data = data
+                {
+                    do {
+                        let fetchedProfileStore = try jsonDecoder.decode([FetchedNightscoutProfileStore].self, from: data)
+                        guard let fetchedProfile: ScheduledNightscoutProfile = fetchedProfileStore.first?.store["default"]
+                        else {
+                            error = "\nCan't find the default Nightscout Profile."
+                            group.leave()
+                            return
+                        }
+
+                        guard fetchedProfile.units.contains(self.units.rawValue.prefix(4)) else {
+                            debug(
+                                .nightscout,
+                                "Mismatching glucose units in Nightscout and Pump Settings. Import settings aborted."
+                            )
+                            error = "\nMismatching glucose units in Nightscout and Pump Settings. Import settings aborted."
+                            group.leave()
+                            return
+                        }
+
+                        var areCRsOK = true
+                        let carbratios = fetchedProfile.carbratio
+                            .map { carbratio -> CarbRatioEntry in
+                                if carbratio.value <= 0 {
+                                    error =
+                                        "\nInvalid Carb Ratio settings in Nightscout.\n\nImport aborted. Please check your Nightscout Profile Carb Ratios Settings!"
+                                    areCRsOK = false
+                                }
+                                return CarbRatioEntry(
+                                    start: carbratio.time,
+                                    offset: self.offset(carbratio.time) / 60,
+                                    ratio: carbratio.value
+                                ) }
+                        let carbratiosProfile = CarbRatios(units: CarbUnit.grams, schedule: carbratios)
+                        guard areCRsOK else {
+                            group.leave()
+                            return
+                        }
+
+                        var areBasalsOK = true
+                        let pumpName = self.apsManager.pumpName.value
+                        let basals = fetchedProfile.basal
+                            .map { basal -> BasalProfileEntry in
+                                if pumpName != "Omnipod DASH", basal.value <= 0
+                                {
+                                    error =
+                                        "\nInvalid Nightcsout Basal Settings. Some or all of your basal settings are 0 U/h.\n\nImport aborted. Please check your Nightscout Profile Basal Settings before trying to import again. Import has been aborted.)"
+                                    areBasalsOK = false
+                                }
+                                return BasalProfileEntry(
+                                    start: basal.time,
+                                    minutes: self.offset(basal.time) / 60,
+                                    rate: basal.value
+                                ) }
+                        // DASH pumps can have 0U/h basal rates but don't import if total basals (24 hours) amount to 0 U.
+                        if pumpName == "Omnipod DASH", basals.map({ each in each.rate }).reduce(0, +) <= 0
+                        {
+                            error =
+                                "\nYour total Basal insulin amount to 0 U or lower in Nightscout Profile settings.\n\n Please check your Nightscout Profile Basal Settings before trying to import again. Import has been aborted.)"
+                            areBasalsOK = false
+                        }
+                        guard areBasalsOK else {
+                            group.leave()
+                            return
+                        }
+
+                        let sensitivities = fetchedProfile.sens.map { sensitivity -> InsulinSensitivityEntry in
+                            InsulinSensitivityEntry(
+                                sensitivity: self.units == .mmolL ? sensitivity.value : sensitivity.value.asMgdL,
+                                offset: self.offset(sensitivity.time) / 60,
+                                start: sensitivity.time
+                            )
+                        }
+                        if sensitivities.filter({ $0.sensitivity <= 0 }).isNotEmpty {
+                            error =
+                                "\nInvalid Nightcsout Sensitivities Settings. \n\nImport aborted. Please check your Nightscout Profile Sensitivities Settings!"
+                            group.leave()
+                            return
+                        }
+
+                        let sensitivitiesProfile = InsulinSensitivities(
+                            units: self.units,
+                            userPrefferedUnits: self.units,
+                            sensitivities: sensitivities
+                        )
+
+                        let targets = fetchedProfile.target_low
+                            .map { target -> BGTargetEntry in
+                                BGTargetEntry(
+                                    low: self.units == .mmolL ? target.value : target.value.asMgdL,
+                                    high: self.units == .mmolL ? target.value : target.value.asMgdL,
+                                    start: target.time,
+                                    offset: self.offset(target.time) / 60
+                                ) }
+                        let targetsProfile = BGTargets(
+                            units: self.units,
+                            userPrefferedUnits: self.units,
+                            targets: targets
+                        )
+                        // IS THERE A PUMP?
+                        guard let pump = self.apsManager.pumpManager else {
+                            self.storage.save(carbratiosProfile, as: OpenAPS.Settings.carbRatios)
+                            self.storage.save(basals, as: OpenAPS.Settings.basalProfile)
+                            self.storage.save(sensitivitiesProfile, as: OpenAPS.Settings.insulinSensitivities)
+                            self.storage.save(targetsProfile, as: OpenAPS.Settings.bgTargets)
+                            debug(
+                                .service,
+                                "Settings were imported but the Basals couldn't be saved to pump (No pump). Check your basal settings and tap ´Save on Pump´ to sync the new basal settings"
+                            )
+                            error =
+                                "\nSettings were imported but the Basals couldn't be saved to pump (No pump). Check your basal settings and tap ´Save on Pump´ to sync the new basal settings"
+                            group.leave()
+                            return
+                        }
+                        let syncValues = basals.map {
+                            RepeatingScheduleValue(startTime: TimeInterval($0.minutes * 60), value: Double($0.rate))
+                        }
+                        // SSAVE TO STORAGE. SAVE TO PUMP (LoopKit)
+                        pump.syncBasalRateSchedule(items: syncValues) { result in
+                            switch result {
+                            case .success:
+                                self.storage.save(basals, as: OpenAPS.Settings.basalProfile)
+                                self.storage.save(carbratiosProfile, as: OpenAPS.Settings.carbRatios)
+                                self.storage.save(sensitivitiesProfile, as: OpenAPS.Settings.insulinSensitivities)
+                                self.storage.save(targetsProfile, as: OpenAPS.Settings.bgTargets)
+                                debug(.service, "Settings have been imported and the Basals saved to pump!")
+                                // DIA. Save if changed.
+                                let dia = fetchedProfile.dia
+                                print("dia: " + dia.description)
+                                print("pump dia: " + self.dia.description)
+                                if dia != self.dia, dia >= 0 {
+                                    let file = PumpSettings(
+                                        insulinActionCurve: dia,
+                                        maxBolus: self.maxBolus,
+                                        maxBasal: self.maxBasal
+                                    )
+                                    self.storage.save(file, as: OpenAPS.Settings.settings)
+                                    debug(.nightscout, "DIA setting updated to " + dia.description + " after a NS import.")
+                                }
+                                group.leave()
+                            case .failure:
+                                error =
+                                    "\nSettings were imported but the Basals couldn't be saved to pump (communication error). Check your basal settings and tap ´Save on Pump´ to sync the new basal settings"
+                                debug(.service, "Basals couldn't be save to pump")
+                                group.leave()
+                            }
+                        }
+                    } catch let parsingError {
+                        print(parsingError)
+                        error = parsingError.localizedDescription
+                        group.leave()
+                    }
+                }
+            }
+            task.resume()
+            group.wait(wallTimeout: .now() + 5)
+            group.notify(queue: .global(qos: .background)) {
+                self.saveError(error)
+            }
+        }
+
+        func offset(_ string: String) -> Int {
+            let hours = Int(string.prefix(2)) ?? 0
+            let minutes = Int(string.suffix(2)) ?? 0
+            return ((hours * 60) + minutes) * 60
+        }
+
+        func saveError(_ string: String) {
+            coredataContext.performAndWait {
+                let saveToCoreData = ImportError(context: self.coredataContext)
+                saveToCoreData.date = Date()
+                saveToCoreData.error = string
+                if coredataContext.hasChanges {
+                    try? coredataContext.save()
+                }
+            }
+        }
+
         func backfillGlucose() {
         func backfillGlucose() {
             backfilling = true
             backfilling = true
             nightscoutManager.fetchGlucose(since: Date().addingTimeInterval(-1.days.timeInterval))
             nightscoutManager.fetchGlucose(since: Date().addingTimeInterval(-1.days.timeInterval))

+ 64 - 3
FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutConfigRootView.swift

@@ -1,3 +1,4 @@
+import CoreData
 import SwiftUI
 import SwiftUI
 import Swinject
 import Swinject
 
 
@@ -5,6 +6,16 @@ extension NightscoutConfig {
     struct RootView: BaseView {
     struct RootView: BaseView {
         let resolver: Resolver
         let resolver: Resolver
         @StateObject var state = StateModel()
         @StateObject var state = StateModel()
+        @State var importAlert: Alert?
+        @State var isImportAlertPresented = false
+        @State var importedHasRun = false
+
+        @FetchRequest(
+            entity: ImportError.entity(),
+            sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)], predicate: NSPredicate(
+                format: "date > %@", Date().addingTimeInterval(-1.minutes.timeInterval) as NSDate
+            )
+        ) var fetchedErrors: FetchedResults<ImportError>
 
 
         private var portFormater: NumberFormatter {
         private var portFormater: NumberFormatter {
             let formatter = NumberFormatter()
             let formatter = NumberFormatter()
@@ -53,22 +64,72 @@ extension NightscoutConfig {
                     Text("Allow Uploads")
                     Text("Allow Uploads")
                 }
                 }
 
 
-                Section(header: Text("Local glucose source")) {
+                Section {
+                    Button("Import settings from Nightscout") {
+                        importAlert = Alert(
+                            title: Text("Import settings?"),
+                            message: Text(
+                                "\n" +
+                                    NSLocalizedString(
+                                        "This will replace some or all of your current pump settings. Are you sure you want to import profile settings from Nightscout?",
+                                        comment: "Profile Import Alert"
+                                    ) +
+                                    "\n"
+                            ),
+                            primaryButton: .destructive(
+                                Text("Yes, Import"),
+                                action: {
+                                    state.importSettings()
+                                    importedHasRun = true
+                                }
+                            ),
+                            secondaryButton: .cancel()
+                        )
+                        isImportAlertPresented.toggle()
+                    }.disabled(state.url.isEmpty || state.connecting)
+
+                } header: { Text("Import from Nightscout") }
+
+                    .alert(isPresented: $importedHasRun) {
+                        Alert(
+                            title: Text((fetchedErrors.first?.error ?? "").count < 4 ? "Settings imported" : "Import Error"),
+                            message: Text(
+                                (fetchedErrors.first?.error ?? "").count < 4 ?
+                                    NSLocalizedString(
+                                        "\nNow please verify all of your new settings thoroughly:\n\n* Basal Settings\n * Carb Ratios\n * Glucose Targets\n * Insulin Sensitivities\n * DIA\n\n in iAPS Settings > Configuration.\n\nBad or invalid profile settings could have disatrous effects.",
+                                        comment: "Imported Profiles Alert"
+                                    ) :
+                                    NSLocalizedString(fetchedErrors.first?.error ?? "", comment: "Import Error")
+                            ),
+                            primaryButton: .destructive(
+                                Text("OK")
+                            ),
+                            secondaryButton: .cancel()
+                        )
+                    }
+
+                Section {
                     Toggle("Use local glucose server", isOn: $state.useLocalSource)
                     Toggle("Use local glucose server", isOn: $state.useLocalSource)
                     HStack {
                     HStack {
                         Text("Port")
                         Text("Port")
                         DecimalTextField("", value: $state.localPort, formatter: portFormater)
                         DecimalTextField("", value: $state.localPort, formatter: portFormater)
                     }
                     }
-                }
-
+                } header: { Text("Local glucose source") }
                 Section {
                 Section {
                     Button("Backfill glucose") { state.backfillGlucose() }
                     Button("Backfill glucose") { state.backfillGlucose() }
                         .disabled(state.url.isEmpty || state.connecting || state.backfilling)
                         .disabled(state.url.isEmpty || state.connecting || state.backfilling)
                 }
                 }
+
+                Section {
+                    Toggle("Remote control", isOn: $state.allowAnnouncements)
+                } header: { Text("Allow Remote control of iAPS") }
             }
             }
             .onAppear(perform: configureView)
             .onAppear(perform: configureView)
             .navigationBarTitle("Nightscout Config")
             .navigationBarTitle("Nightscout Config")
             .navigationBarTitleDisplayMode(.automatic)
             .navigationBarTitleDisplayMode(.automatic)
+            .alert(isPresented: $isImportAlertPresented) {
+                importAlert!
+            }
         }
         }
     }
     }
 }
 }

+ 3 - 4
FreeAPS/Sources/Modules/Settings/SettingsStateModel.swift

@@ -65,13 +65,12 @@ extension Settings {
             return items
             return items
         }
         }
 
 
-        func uploadProfile() {
-            NSLog("SettingsState Upload Profile")
-            nightscoutManager.uploadProfile()
+        func uploadProfileAndSettings(_ force: Bool) {
+            NSLog("SettingsState Upload Profile and Settings")
+            nightscoutManager.uploadProfileAndSettings(force)
         }
         }
 
 
         func hideSettingsModal() {
         func hideSettingsModal() {
-            nightscoutManager.uploadProfile()
             hideModal()
             hideModal()
         }
         }
     }
     }

+ 6 - 4
FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift

@@ -49,11 +49,12 @@ extension Settings {
                     Toggle("Debug options", isOn: $state.debugOptions)
                     Toggle("Debug options", isOn: $state.debugOptions)
                     if state.debugOptions {
                     if state.debugOptions {
                         Group {
                         Group {
-                            Text("NS Upload Profile").onTapGesture {
-                                state.uploadProfile()
+                            HStack {
+                                Text("NS Upload Profile and Settings")
+                                Button("Upload") { state.uploadProfileAndSettings(true) }
+                                    .frame(maxWidth: .infinity, alignment: .trailing)
+                                    .buttonStyle(.borderedProminent)
                             }
                             }
-                            Text("NS Uploaded Profile")
-                                .navigationLink(to: .configEditor(file: OpenAPS.Nightscout.uploadedProfile), from: self)
                         }
                         }
                         Group {
                         Group {
                             Text("Preferences")
                             Text("Preferences")
@@ -126,6 +127,7 @@ extension Settings {
             .navigationTitle("Settings")
             .navigationTitle("Settings")
             .navigationBarItems(leading: Button("Close", action: state.hideSettingsModal))
             .navigationBarItems(leading: Button("Close", action: state.hideSettingsModal))
             .navigationBarTitleDisplayMode(.automatic)
             .navigationBarTitleDisplayMode(.automatic)
+            .onDisappear(perform: { state.uploadProfileAndSettings(false) })
         }
         }
     }
     }
 }
 }

+ 28 - 0
FreeAPS/Sources/Services/Network/NightscoutAPI.swift

@@ -1,6 +1,8 @@
 import Combine
 import Combine
 import CommonCrypto
 import CommonCrypto
 import Foundation
 import Foundation
+import JavaScriptCore
+import Swinject
 
 
 class NightscoutAPI {
 class NightscoutAPI {
     init(url: URL, secret: String? = nil) {
     init(url: URL, secret: String? = nil) {
@@ -27,6 +29,8 @@ class NightscoutAPI {
     let secret: String?
     let secret: String?
 
 
     private let service = NetworkService()
     private let service = NetworkService()
+
+    @Injected() private var settingsManager: SettingsManager!
 }
 }
 
 
 extension NightscoutAPI {
 extension NightscoutAPI {
@@ -394,6 +398,30 @@ extension NightscoutAPI {
             .eraseToAnyPublisher()
             .eraseToAnyPublisher()
     }
     }
 
 
+    func uploadSettings(_ settings: NightscoutSettings) -> AnyPublisher<Void, Swift.Error> {
+        var components = URLComponents()
+        components.scheme = url.scheme
+        components.host = url.host
+        components.port = url.port
+        components.path = Config.statusPath
+
+        var request = URLRequest(url: components.url!)
+        request.allowsConstrainedNetworkAccess = false
+        request.timeoutInterval = Config.timeout
+        request.addValue("application/json", forHTTPHeaderField: "Content-Type")
+
+        if let secret = secret {
+            request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
+        }
+        request.httpBody = try! JSONCoding.encoder.encode(settings)
+        request.httpMethod = "POST"
+
+        return service.run(request)
+            .retry(Config.retryCount)
+            .map { _ in () }
+            .eraseToAnyPublisher()
+    }
+
     func uploadProfile(_ profile: NightscoutProfileStore) -> AnyPublisher<Void, Swift.Error> {
     func uploadProfile(_ profile: NightscoutProfileStore) -> AnyPublisher<Void, Swift.Error> {
         var components = URLComponents()
         var components = URLComponents()
         components.scheme = url.scheme
         components.scheme = url.scheme

+ 95 - 38
FreeAPS/Sources/Services/Network/NightscoutManager.swift

@@ -12,10 +12,10 @@ protocol NightscoutManager: GlucoseSource {
     func deleteCarbs(at date: Date, isFPU: Bool?, fpuID: String?, syncID: String)
     func deleteCarbs(at date: Date, isFPU: Bool?, fpuID: String?, syncID: String)
     func deleteInsulin(at date: Date)
     func deleteInsulin(at date: Date)
     func uploadStatus()
     func uploadStatus()
-    func uploadStatistics(dailystat: Statistics)
-    func uploadPreferences()
     func uploadGlucose()
     func uploadGlucose()
-    func uploadProfile()
+    func uploadStatistics(dailystat: Statistics)
+    func uploadPreferences(_ preferences: Preferences)
+    func uploadProfileAndSettings(_: Bool)
     var cgmURL: URL? { get }
     var cgmURL: URL? { get }
 }
 }
 
 
@@ -274,7 +274,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         }
         }
     }
     }
 
 
-    func uploadPreferences() {
+    func uploadPreferences(_ preferences: Preferences) {
         let prefs = NightscoutPreferences(
         let prefs = NightscoutPreferences(
             preferences: settingsManager.preferences
             preferences: settingsManager.preferences
         )
         )
@@ -289,6 +289,31 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                     switch completion {
                     switch completion {
                     case .finished:
                     case .finished:
                         debug(.nightscout, "Preferences uploaded")
                         debug(.nightscout, "Preferences uploaded")
+                        self.storage.save(preferences, as: OpenAPS.Nightscout.uploadedPreferences)
+                    case let .failure(error):
+                        debug(.nightscout, error.localizedDescription)
+                    }
+                } receiveValue: {}
+                .store(in: &self.lifetime)
+        }
+    }
+
+    func uploadSettings(_ settings: FreeAPSSettings) {
+        let sets = NightscoutSettings(
+            settings: settingsManager.settings
+        )
+
+        guard let nightscout = nightscoutAPI, isUploadEnabled else {
+            return
+        }
+
+        processQueue.async {
+            nightscout.uploadSettings(sets)
+                .sink { completion in
+                    switch completion {
+                    case .finished:
+                        debug(.nightscout, "Settings uploaded")
+                        self.storage.save(settings, as: OpenAPS.Nightscout.uploadedSettings)
                     case let .failure(error):
                     case let .failure(error):
                         debug(.nightscout, error.localizedDescription)
                         debug(.nightscout, error.localizedDescription)
                     }
                     }
@@ -401,14 +426,29 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         }
         }
     }
     }
 
 
-    func uploadProfile() {
-        // These should be modified anyways and not the defaults
-        guard let sensitivities = storage.retrieve(OpenAPS.Settings.insulinSensitivities, as: InsulinSensitivities.self),
-              let basalProfile = storage.retrieve(OpenAPS.Settings.basalProfile, as: [BasalProfileEntry].self),
-              let carbRatios = storage.retrieve(OpenAPS.Settings.carbRatios, as: CarbRatios.self),
-              let targets = storage.retrieve(OpenAPS.Settings.bgTargets, as: BGTargets.self)
-        else {
-            NSLog("NightscoutManager uploadProfile Not all settings found to build profile!")
+    func uploadProfileAndSettings(_ force: Bool) {
+        guard let sensitivities = storage.retrieve(OpenAPS.Settings.insulinSensitivities, as: InsulinSensitivities.self) else {
+            debug(.nightscout, "NightscoutManager uploadProfile: error loading insulinSensitivities")
+            return
+        }
+        guard let settings = storage.retrieve(OpenAPS.FreeAPS.settings, as: FreeAPSSettings.self) else {
+            debug(.nightscout, "NightscoutManager uploadProfile: error loading settings")
+            return
+        }
+        guard let preferences = storage.retrieve(OpenAPS.Settings.preferences, as: Preferences.self) else {
+            debug(.nightscout, "NightscoutManager uploadProfile: error loading preferences")
+            return
+        }
+        guard let targets = storage.retrieve(OpenAPS.Settings.bgTargets, as: BGTargets.self) else {
+            debug(.nightscout, "NightscoutManager uploadProfile: error loading bgTargets")
+            return
+        }
+        guard let carbRatios = storage.retrieve(OpenAPS.Settings.carbRatios, as: CarbRatios.self) else {
+            debug(.nightscout, "NightscoutManager uploadProfile: error loading carbRatios")
+            return
+        }
+        guard let basalProfile = storage.retrieve(OpenAPS.Settings.basalProfile, as: [BasalProfileEntry].self) else {
+            debug(.nightscout, "NightscoutManager uploadProfile: error loading basalProfile")
             return
             return
         }
         }
 
 
@@ -416,32 +456,30 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             NightscoutTimevalue(
             NightscoutTimevalue(
                 time: String(item.start.prefix(5)),
                 time: String(item.start.prefix(5)),
                 value: item.sensitivity,
                 value: item.sensitivity,
-                timeAsSeconds: item.offset
+                timeAsSeconds: item.offset * 60
             )
             )
         }
         }
-
         let target_low = targets.targets.map { item -> NightscoutTimevalue in
         let target_low = targets.targets.map { item -> NightscoutTimevalue in
             NightscoutTimevalue(
             NightscoutTimevalue(
                 time: String(item.start.prefix(5)),
                 time: String(item.start.prefix(5)),
                 value: item.low,
                 value: item.low,
-                timeAsSeconds: item.offset
+                timeAsSeconds: item.offset * 60
             )
             )
         }
         }
         let target_high = targets.targets.map { item -> NightscoutTimevalue in
         let target_high = targets.targets.map { item -> NightscoutTimevalue in
             NightscoutTimevalue(
             NightscoutTimevalue(
                 time: String(item.start.prefix(5)),
                 time: String(item.start.prefix(5)),
                 value: item.high,
                 value: item.high,
-                timeAsSeconds: item.offset
+                timeAsSeconds: item.offset * 60
             )
             )
         }
         }
         let cr = carbRatios.schedule.map { item -> NightscoutTimevalue in
         let cr = carbRatios.schedule.map { item -> NightscoutTimevalue in
             NightscoutTimevalue(
             NightscoutTimevalue(
                 time: String(item.start.prefix(5)),
                 time: String(item.start.prefix(5)),
                 value: item.ratio,
                 value: item.ratio,
-                timeAsSeconds: item.offset
+                timeAsSeconds: item.offset * 60
             )
             )
         }
         }
-
         let basal = basalProfile.map { item -> NightscoutTimevalue in
         let basal = basalProfile.map { item -> NightscoutTimevalue in
             NightscoutTimevalue(
             NightscoutTimevalue(
                 time: String(item.start.prefix(5)),
                 time: String(item.start.prefix(5)),
@@ -471,6 +509,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             // No, Decimal has no rounding function.
             // No, Decimal has no rounding function.
             carbs_hr = Decimal(round(Double(carbs_hr) * 10.0)) / 10
             carbs_hr = Decimal(round(Double(carbs_hr) * 10.0)) / 10
         }
         }
+
         let ps = ScheduledNightscoutProfile(
         let ps = ScheduledNightscoutProfile(
             dia: settingsManager.pumpSettings.insulinActionCurve,
             dia: settingsManager.pumpSettings.insulinActionCurve,
             carbs_hr: Int(carbs_hr),
             carbs_hr: Int(carbs_hr),
@@ -484,6 +523,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             units: nsUnits
             units: nsUnits
         )
         )
         let defaultProfile = "default"
         let defaultProfile = "default"
+
         let now = Date()
         let now = Date()
         let p = NightscoutProfileStore(
         let p = NightscoutProfileStore(
             defaultProfile: defaultProfile,
             defaultProfile: defaultProfile,
@@ -494,33 +534,48 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             store: [defaultProfile: ps]
             store: [defaultProfile: ps]
         )
         )
 
 
-        if let uploadedProfile = storage.retrieve(OpenAPS.Nightscout.uploadedProfile, as: NightscoutProfileStore.self),
-           (uploadedProfile.store[defaultProfile]?.rawJSON ?? "") == ps.rawJSON
-        {
-            NSLog("NightscoutManager uploadProfile, no profile change")
-            return
-        }
         guard let nightscout = nightscoutAPI, isNetworkReachable, isUploadEnabled else {
         guard let nightscout = nightscoutAPI, isNetworkReachable, isUploadEnabled else {
             return
             return
         }
         }
-        processQueue.async {
-            nightscout.uploadProfile(p)
-                .sink { completion in
-                    switch completion {
-                    case .finished:
-                        self.storage.save(p, as: OpenAPS.Nightscout.uploadedProfile)
-                        debug(.nightscout, "Profile uploaded")
-                    case let .failure(error):
-                        debug(.nightscout, error.localizedDescription)
-                    }
-                } receiveValue: {}
-                .store(in: &self.lifetime)
+
+        // UPLOAD PREFERNCES WHEN CHANGED
+        if let uploadedPreferences = storage.retrieve(OpenAPS.Nightscout.uploadedPreferences, as: Preferences.self),
+           uploadedPreferences.rawJSON.sorted() == preferences.rawJSON.sorted(), !force
+        {
+            NSLog("NightscoutManager Preferences, preferences unchanged")
+        } else { uploadPreferences(preferences) }
+
+        // UPLOAD FreeAPS Settings WHEN CHANGED
+        if let uploadedSettings = storage.retrieve(OpenAPS.Nightscout.uploadedSettings, as: FreeAPSSettings.self),
+           uploadedSettings.rawJSON.sorted() == settings.rawJSON.sorted(), !force
+        {
+            NSLog("NightscoutManager Settings, settings unchanged")
+        } else { uploadSettings(settings) }
+
+        // UPLOAD Profiles WHEN CHANGED
+        if let uploadedProfile = storage.retrieve(OpenAPS.Nightscout.uploadedProfile, as: NightscoutProfileStore.self),
+           (uploadedProfile.store["default"]?.rawJSON ?? "").sorted() == ps.rawJSON.sorted(), !force
+        {
+            NSLog("NightscoutManager uploadProfile, no profile change")
+        } else {
+            processQueue.async {
+                nightscout.uploadProfile(p)
+                    .sink { completion in
+                        switch completion {
+                        case .finished:
+                            self.storage.save(p, as: OpenAPS.Nightscout.uploadedProfile)
+                            debug(.nightscout, "Profile uploaded")
+                        case let .failure(error):
+                            debug(.nightscout, error.localizedDescription)
+                        }
+                    } receiveValue: {}
+                    .store(in: &self.lifetime)
+            }
         }
         }
     }
     }
 
 
     func uploadGlucose() {
     func uploadGlucose() {
         uploadGlucose(glucoseStorage.nightscoutGlucoseNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedGlucose)
         uploadGlucose(glucoseStorage.nightscoutGlucoseNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedGlucose)
-
         uploadTreatments(glucoseStorage.nightscoutCGMStateNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedCGMState)
         uploadTreatments(glucoseStorage.nightscoutCGMStateNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedCGMState)
     }
     }
 
 
@@ -560,8 +615,9 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                     switch completion {
                     switch completion {
                     case .finished:
                     case .finished:
                         self.storage.save(glucose, as: fileToSave)
                         self.storage.save(glucose, as: fileToSave)
+                        debug(.nightscout, "Glucose uploaded")
                     case let .failure(error):
                     case let .failure(error):
-                        debug(.nightscout, error.localizedDescription)
+                        debug(.nightscout, "Upload of glucose failed: " + error.localizedDescription)
                     }
                     }
                 } receiveValue: {}
                 } receiveValue: {}
                 .store(in: &self.lifetime)
                 .store(in: &self.lifetime)
@@ -589,6 +645,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                     switch completion {
                     switch completion {
                     case .finished:
                     case .finished:
                         self.storage.save(treatments, as: fileToSave)
                         self.storage.save(treatments, as: fileToSave)
+                        debug(.nightscout, "Treatments uploaded")
                     case let .failure(error):
                     case let .failure(error):
                         debug(.nightscout, error.localizedDescription)
                         debug(.nightscout, error.localizedDescription)
                     }
                     }

+ 2 - 1
FreeAPS/Sources/Views/TagCloudView.swift

@@ -66,7 +66,8 @@ struct TagCloudView: View {
                  textTag where textTag.contains("AF:"),
                  textTag where textTag.contains("AF:"),
                  textTag where textTag.contains("Autosens/Dynamic Limit:"),
                  textTag where textTag.contains("Autosens/Dynamic Limit:"),
                  textTag where textTag.contains("Dynamic ISF/CR"),
                  textTag where textTag.contains("Dynamic ISF/CR"),
-                 textTag where textTag.contains("Basal ratio"):
+                 textTag where textTag.contains("Basal ratio"),
+                 textTag where textTag.contains("SMB Ratio"):
                 return .zt
                 return .zt
             default:
             default:
                 return .insulin
                 return .insulin