Prechádzať zdrojové kódy

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 rokov pred
rodič
commit
40e3fad664

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

@@ -24,6 +24,10 @@ jobs:
       - name: Patch Match Tables
         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
       - name: Fastlane Provision
         run: fastlane identifiers

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

@@ -34,6 +34,10 @@ jobs:
       - name: Patch Match Tables
         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
       - name: Fastlane Build & Archive
         run: fastlane build_iAPS

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

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

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

@@ -10,6 +10,10 @@ jobs:
       - name: Checkout Repo
         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
       - name: Validate Secrets
         run: |

+ 1 - 1
Config.xcconfig

@@ -1,5 +1,5 @@
 APP_DISPLAY_NAME = iAPS
-APP_VERSION = 2.2.8
+APP_VERSION = 2.3.0
 APP_BUILD_NUMBER = 1
 COPYRIGHT_NOTICE =
 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_30" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
     </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">
         <attribute name="bolus" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
         <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 */; };
 		190EBCC829FF13AA00BA767D /* StatConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 190EBCC729FF13AA00BA767D /* StatConfigStateModel.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 */; };
 		1935364028496F7D001E0B16 /* Oref2_variables.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1935363F28496F7D001E0B16 /* Oref2_variables.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 */; };
 		C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFCFE0781F9074C2917890E8 /* ManualTempBasalStateModel.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 */; };
 		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 */; };
@@ -492,6 +494,7 @@
 		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>"; };
 		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>"; };
 		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>"; };
@@ -818,6 +821,7 @@
 		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>"; };
 		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>"; };
 		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>"; };
@@ -1602,10 +1606,12 @@
 				19012CDB291D2CB900FB8210 /* LoopStats.swift */,
 				FE41E4D329463C660047FD55 /* NightscoutStatistics.swift */,
 				FE41E4D529463EE20047FD55 /* NightscoutPreferences.swift */,
+				191F62672AD6B05A004D7911 /* NightscoutSettings.swift */,
 				1967DFBD29D052C200759F30 /* Icons.swift */,
 				19D4E4EA29FC6A9F00351451 /* TIRforChart.swift */,
 				19A910352A24D6D700C8951B /* DateFilter.swift */,
 				193F6CDC2A512C8F001240FD /* Loops.swift */,
+				CC6C406D2ACDD69E009B8058 /* RawFetchedProfile.swift */,
 			);
 			path = Models;
 			sourceTree = "<group>";
@@ -2619,6 +2625,7 @@
 				38A9260525F012D8009E3739 /* CarbRatios.swift in Sources */,
 				38FCF3D625E8FDF40078B0D1 /* MD5.swift in Sources */,
 				3871F39C25ED892B0013ECB5 /* TempTarget.swift in Sources */,
+				191F62682AD6B05A004D7911 /* NightscoutSettings.swift in Sources */,
 				FEFA5C11299F814A00765C17 /* CoreDataStack.swift in Sources */,
 				3811DEAB25C9D88300A708ED /* HTTPResponseStatus.swift in Sources */,
 				3811DE5F25C9D4D500A708ED /* ProgressBar.swift in Sources */,
@@ -2693,6 +2700,7 @@
 				63E890B4D951EAA91C071D5C /* BasalProfileEditorStateModel.swift in Sources */,
 				CE398D16297C9D1D00DF218F /* dexcomSourceG7.swift in Sources */,
 				38FEF3FA2737E42000574A46 /* BaseStateModel.swift in Sources */,
+				CC6C406E2ACDD69E009B8058 /* RawFetchedProfile.swift in Sources */,
 				385CEA8225F23DFD002D6D5B /* NightscoutStatus.swift in Sources */,
 				F90692AA274B7AAE0037068D /* HealthKitManager.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-space" : "srgb",
         "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"

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

@@ -1192,19 +1192,15 @@ final class BaseAPSManager: APSManager, Injectable {
                 )
                 storage.save(dailystat, as: file)
                 nightscout.uploadStatistics(dailystat: dailystat)
-                nightscout.uploadPreferences()
 
                 let saveStatsCoreData = StatsData(context: self.coredataContext)
                 saveStatsCoreData.lastrun = Date()
                 try? self.coredataContext.save()
-                print("Test time of statistics computation: \(-1 * now.timeIntervalSinceNow) s")
             }
         }
     }
 
     private func loopStats(loopStatRecord: LoopStats) {
-        let LoopStatsStartedAt = Date()
-
         coredataContext.perform {
             let nLS = LoopStatRecord(context: self.coredataContext)
 
@@ -1216,8 +1212,6 @@ final class BaseAPSManager: APSManager, Injectable {
 
             try? self.coredataContext.save()
         }
-        print("LoopStatRecords: \(loopStatRecord)")
-        print("Test time of LoopStats computation: \(-1 * LoopStatsStartedAt.timeIntervalSinceNow) s")
     }
 
     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 cgmState = "monitor/cgm-state.json"
         static let podAge = "monitor/pod-age.json"
-        // static let tdd = "monitor/tdd.json"
         static let oref2_variables = "monitor/oref2_variables.json"
         static let alertHistory = "monitor/alerthistory.json"
         static let statistics = "monitor/statistics.json"
@@ -86,6 +85,8 @@ extension OpenAPS {
         static let uploadedCGMState = "upload/uploaded-cgm-state.json"
         static let uploadedPodAge = "upload/uploaded-pod-age.json"
         static let uploadedProfile = "upload/uploaded-profile.json"
+        static let uploadedPreferences = "upload/uploaded-preferences.json"
+        static let uploadedSettings = "upload/uploaded-settings.json"
     }
 
     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
 
 struct NightscoutPreferences: JSON {
-    let report = "preferences"
+    var report = "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 {
     let time: String
     let value: Decimal
-    let timeAsSeconds: Int
+    let timeAsSeconds: Int?
 }
 
 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 LoopKit
 import SwiftUI
 
 extension AutotuneConfig {
     final class StateModel: BaseStateModel<Provider> {
         @Injected() var apsManager: APSManager!
+        @Injected() private var storage: FileStorage!
         @Published var useAutotune = false
         @Published var onlyAutotuneBasals = false
         @Published var autotune: Autotune?
@@ -59,5 +61,35 @@ extension AutotuneConfig {
                 .cancellable()
                 .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 {
         let resolver: Resolver
         @StateObject var state = StateModel()
+        @State var replaceAlert = false
 
         private var isfFormatter: NumberFormatter {
             let formatter = NumberFormatter()
@@ -94,11 +95,27 @@ extension AutotuneConfig {
                         label: { Text("Delete autotune data") }
                             .foregroundColor(.red)
                     }
+
+                    Section {
+                        Button {
+                            replaceAlert = true
+                        }
+                        label: { Text("Save as your Normal Basal Rates") }
+                    } header: {
+                        Text("Replace Normal Basal")
+                    }
                 }
             }
             .onAppear(perform: configureView)
             .navigationTitle("Autotune")
             .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 Combine
+import CoreData
 import G7SensorKit
+import LoopKit
 import SwiftDate
 import SwiftUI
 
@@ -11,6 +13,10 @@ extension NightscoutConfig {
         @Injected() private var glucoseStorage: GlucoseStorage!
         @Injected() private var healthKitManager: HealthKitManager!
         @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 secret = ""
@@ -22,11 +28,21 @@ extension NightscoutConfig {
         @Published var uploadGlucose = true // Upload Glucose
         @Published var useLocalSource = false
         @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() {
             url = keychain.getValue(String.self, forKey: Config.urlKey) ?? ""
             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(\.useLocalGlucoseSource, on: $useLocalSource) { useLocalSource = $0 }
             subscribeSetting(\.localGlucosePort, on: $localPort.map(Int.init)) { localPort = Decimal($0) }
@@ -45,10 +61,6 @@ extension NightscoutConfig {
         }
 
         func connect() {
-            if let CheckURL = url.last, CheckURL == "/" {
-                let fixedURL = url.dropLast()
-                url = String(fixedURL)
-            }
             guard let url = URL(string: url) else {
                 message = "Invalid URL"
                 return
@@ -72,6 +84,240 @@ extension NightscoutConfig {
                 .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() {
             backfilling = true
             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 Swinject
 
@@ -5,6 +6,16 @@ extension NightscoutConfig {
     struct RootView: BaseView {
         let resolver: Resolver
         @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 {
             let formatter = NumberFormatter()
@@ -53,22 +64,72 @@ extension NightscoutConfig {
                     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)
                     HStack {
                         Text("Port")
                         DecimalTextField("", value: $state.localPort, formatter: portFormater)
                     }
-                }
-
+                } header: { Text("Local glucose source") }
                 Section {
                     Button("Backfill glucose") { state.backfillGlucose() }
                         .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)
             .navigationBarTitle("Nightscout Config")
             .navigationBarTitleDisplayMode(.automatic)
+            .alert(isPresented: $isImportAlertPresented) {
+                importAlert!
+            }
         }
     }
 }

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

@@ -65,13 +65,12 @@ extension Settings {
             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() {
-            nightscoutManager.uploadProfile()
             hideModal()
         }
     }

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

@@ -49,11 +49,12 @@ extension Settings {
                     Toggle("Debug options", isOn: $state.debugOptions)
                     if state.debugOptions {
                         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 {
                             Text("Preferences")
@@ -126,6 +127,7 @@ extension Settings {
             .navigationTitle("Settings")
             .navigationBarItems(leading: Button("Close", action: state.hideSettingsModal))
             .navigationBarTitleDisplayMode(.automatic)
+            .onDisappear(perform: { state.uploadProfileAndSettings(false) })
         }
     }
 }

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

@@ -1,6 +1,8 @@
 import Combine
 import CommonCrypto
 import Foundation
+import JavaScriptCore
+import Swinject
 
 class NightscoutAPI {
     init(url: URL, secret: String? = nil) {
@@ -27,6 +29,8 @@ class NightscoutAPI {
     let secret: String?
 
     private let service = NetworkService()
+
+    @Injected() private var settingsManager: SettingsManager!
 }
 
 extension NightscoutAPI {
@@ -394,6 +398,30 @@ extension NightscoutAPI {
             .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> {
         var components = URLComponents()
         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 deleteInsulin(at date: Date)
     func uploadStatus()
-    func uploadStatistics(dailystat: Statistics)
-    func uploadPreferences()
     func uploadGlucose()
-    func uploadProfile()
+    func uploadStatistics(dailystat: Statistics)
+    func uploadPreferences(_ preferences: Preferences)
+    func uploadProfileAndSettings(_: Bool)
     var cgmURL: URL? { get }
 }
 
@@ -274,7 +274,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         }
     }
 
-    func uploadPreferences() {
+    func uploadPreferences(_ preferences: Preferences) {
         let prefs = NightscoutPreferences(
             preferences: settingsManager.preferences
         )
@@ -289,6 +289,31 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                     switch completion {
                     case .finished:
                         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):
                         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
         }
 
@@ -416,32 +456,30 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             NightscoutTimevalue(
                 time: String(item.start.prefix(5)),
                 value: item.sensitivity,
-                timeAsSeconds: item.offset
+                timeAsSeconds: item.offset * 60
             )
         }
-
         let target_low = targets.targets.map { item -> NightscoutTimevalue in
             NightscoutTimevalue(
                 time: String(item.start.prefix(5)),
                 value: item.low,
-                timeAsSeconds: item.offset
+                timeAsSeconds: item.offset * 60
             )
         }
         let target_high = targets.targets.map { item -> NightscoutTimevalue in
             NightscoutTimevalue(
                 time: String(item.start.prefix(5)),
                 value: item.high,
-                timeAsSeconds: item.offset
+                timeAsSeconds: item.offset * 60
             )
         }
         let cr = carbRatios.schedule.map { item -> NightscoutTimevalue in
             NightscoutTimevalue(
                 time: String(item.start.prefix(5)),
                 value: item.ratio,
-                timeAsSeconds: item.offset
+                timeAsSeconds: item.offset * 60
             )
         }
-
         let basal = basalProfile.map { item -> NightscoutTimevalue in
             NightscoutTimevalue(
                 time: String(item.start.prefix(5)),
@@ -471,6 +509,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             // No, Decimal has no rounding function.
             carbs_hr = Decimal(round(Double(carbs_hr) * 10.0)) / 10
         }
+
         let ps = ScheduledNightscoutProfile(
             dia: settingsManager.pumpSettings.insulinActionCurve,
             carbs_hr: Int(carbs_hr),
@@ -484,6 +523,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             units: nsUnits
         )
         let defaultProfile = "default"
+
         let now = Date()
         let p = NightscoutProfileStore(
             defaultProfile: defaultProfile,
@@ -494,33 +534,48 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             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 {
             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() {
         uploadGlucose(glucoseStorage.nightscoutGlucoseNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedGlucose)
-
         uploadTreatments(glucoseStorage.nightscoutCGMStateNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedCGMState)
     }
 
@@ -560,8 +615,9 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                     switch completion {
                     case .finished:
                         self.storage.save(glucose, as: fileToSave)
+                        debug(.nightscout, "Glucose uploaded")
                     case let .failure(error):
-                        debug(.nightscout, error.localizedDescription)
+                        debug(.nightscout, "Upload of glucose failed: " + error.localizedDescription)
                     }
                 } receiveValue: {}
                 .store(in: &self.lifetime)
@@ -589,6 +645,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                     switch completion {
                     case .finished:
                         self.storage.save(treatments, as: fileToSave)
+                        debug(.nightscout, "Treatments uploaded")
                     case let .failure(error):
                         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("Autosens/Dynamic Limit:"),
                  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
             default:
                 return .insulin