Browse Source

Merge branch 'master' into charts

# Conflicts:
#	FreeAPS.xcodeproj/project.pbxproj
Ivan Valkou 5 years ago
parent
commit
4bf346cddd

+ 45 - 1
FreeAPS.xcodeproj/project.pbxproj

@@ -18,6 +18,7 @@
 		25548F1F0AA8E42FF5F96DBA /* PumpSettingsEditorBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10CAE3534904CDCA0F367017 /* PumpSettingsEditorBuilder.swift */; };
 		28089E07169488CF6DCC2A31 /* AddCarbsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86FC1CFD647CF34508AF9A3B /* AddCarbsRootView.swift */; };
 		2BE9A6FA20875F6F4F9CD461 /* PumpSettingsEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97F14812C1AFED3621165A5 /* PumpSettingsEditorProvider.swift */; };
+		3083261C4B268E353F36CD0B /* AutotuneConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DCCCCE633F5E98E41B0CD3C /* AutotuneConfigDataFlow.swift */; };
 		3340E0D14D4701342D459C95 /* PumpConfigBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = E01C416A0792696C6911C1D7 /* PumpConfigBuilder.swift */; };
 		33E198D3039045D98C3DC5D4 /* AddCarbsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E7C997E56DAF8D28D09014 /* AddCarbsViewModel.swift */; };
 		3811DE0925C9D32F00A708ED /* BaseViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3811DE0525C9D32E00A708ED /* BaseViewModel.swift */; };
@@ -152,6 +153,8 @@
 		3895E4C625B9E00D00214B37 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3895E4C525B9E00D00214B37 /* Preferences.swift */; };
 		389FE32725F3ABE6002E92E0 /* CareKitUI in Frameworks */ = {isa = PBXBuildFile; productRef = 389FE32625F3ABE6002E92E0 /* CareKitUI */; };
 		389FE32A25F3AC44002E92E0 /* GlucoseChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389FE32925F3AC44002E92E0 /* GlucoseChartView.swift */; };
+		38A00B1F25FC00F7006BC0B0 /* Autotune.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A00B1E25FC00F7006BC0B0 /* Autotune.swift */; };
+		38A00B2325FC2B55006BC0B0 /* LRUCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A00B2225FC2B55006BC0B0 /* LRUCache.swift */; };
 		38A0363B25ECF07E00FCBB52 /* GlucoseStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A0363A25ECF07E00FCBB52 /* GlucoseStorage.swift */; };
 		38A0364225ED069400FCBB52 /* TempBasal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A0364125ED069400FCBB52 /* TempBasal.swift */; };
 		38A13D3225E28B4B00EAA382 /* PumpHistoryEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A13D3125E28B4B00EAA382 /* PumpHistoryEvent.swift */; };
@@ -239,13 +242,16 @@
 		7BCFACB97C821041BA43A114 /* ManualTempBasalRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C377490C77661D75E8C50649 /* ManualTempBasalRootView.swift */; };
 		7F7017AA5C69838FB7E6FECE /* TargetsEditorBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3409A5984BB4171EC484266B /* TargetsEditorBuilder.swift */; };
 		88AB39B23C9552BD6E0C9461 /* ISFEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBB3BAE7494CB771ABAC7B8B /* ISFEditorRootView.swift */; };
+		891DECF7BC20968D7F566161 /* AutotuneConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5EF98E22A39CD656A230704 /* AutotuneConfigProvider.swift */; };
 		8B759CFCF47B392BB365C251 /* BasalProfileEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67F94DD2853CF42BA4E30616 /* BasalProfileEditorDataFlow.swift */; };
 		8BC2F5A29AD1ED08AC0EE013 /* AddTempTargetRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9AAB83FB6C3B41EFD1846A0 /* AddTempTargetRootView.swift */; };
+		91732A8060347C0E67024D80 /* AutotuneConfigBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91B260840169E712C05ACC1F /* AutotuneConfigBuilder.swift */; };
 		919DBD08F13BAFB180DF6F47 /* AddTempTargetViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C3B5FD881CA45DFDEE0EDA9 /* AddTempTargetViewModel.swift */; };
 		9702FF92A09C53942F20D7EA /* TargetsEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DD795BA46B193644D48138C /* TargetsEditorRootView.swift */; };
 		97C1388354C7133C1D5ED72A /* PreferencesEditorBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = E08D9D69E5B052E5C9E8BD32 /* PreferencesEditorBuilder.swift */; };
 		9825E5E923F0B8FA80C8C7C7 /* NightscoutConfigViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0A48AE3AC813A49A517846A /* NightscoutConfigViewModel.swift */; };
 		98641AF4F92123DA668AB931 /* CREditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BDC6993C1087310EDFC428 /* CREditorRootView.swift */; };
+		A05235B9112E677ED03B6E8E /* AutotuneConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CF5ACEE1F0859670E71B2C0 /* AutotuneConfigRootView.swift */; };
 		A0B8EC8CC5CD1DD237D1BCD2 /* PumpSettingsEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8C7F882606FF83A21BE00D8 /* PumpSettingsEditorRootView.swift */; };
 		A228DF96647338139F152B15 /* PreferencesEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12204445D7632AF09264A979 /* PreferencesEditorDataFlow.swift */; };
 		A33352ED40476125EBAC6EE0 /* CREditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E22146D3DF4853786C78132 /* CREditorDataFlow.swift */; };
@@ -258,6 +264,7 @@
 		CDB87FA71A93F3739D3D338E /* NightscoutConfigBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 111579A6E3AC6BFA79C4DD43 /* NightscoutConfigBuilder.swift */; };
 		D2165E9D78EFF692C1DED1C6 /* AddTempTargetDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8A42073A2D03A278914448 /* AddTempTargetDataFlow.swift */; };
 		D6DEC113821A7F1056C4AA1E /* NightscoutConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F2A13DF0EDEEEDC4106AA2A /* NightscoutConfigDataFlow.swift */; };
+		D76333C9256787610B3B4875 /* AutotuneConfigViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D295A3F870E826BE371C0BB5 /* AutotuneConfigViewModel.swift */; };
 		DBA5254DBB2586C98F61220C /* ISFEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9F137F126D9F8DEB799F26 /* ISFEditorProvider.swift */; };
 		DD399FB31EACB9343C944C4C /* PreferencesEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA3E609094E064C99A4752C /* PreferencesEditorViewModel.swift */; };
 		E102DE9C3E9C8AEDCB3C61BB /* ConfigEditorBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = E492D5B2EEF2119977EA2CE4 /* ConfigEditorBuilder.swift */; };
@@ -440,6 +447,8 @@
 		389442CA25F65F7100FA1F27 /* NightscoutTreatment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutTreatment.swift; sourceTree = "<group>"; };
 		3895E4C525B9E00D00214B37 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
 		389FE32925F3AC44002E92E0 /* GlucoseChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseChartView.swift; sourceTree = "<group>"; };
+		38A00B1E25FC00F7006BC0B0 /* Autotune.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Autotune.swift; sourceTree = "<group>"; };
+		38A00B2225FC2B55006BC0B0 /* LRUCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LRUCache.swift; sourceTree = "<group>"; };
 		38A0363A25ECF07E00FCBB52 /* GlucoseStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseStorage.swift; sourceTree = "<group>"; };
 		38A0364125ED069400FCBB52 /* TempBasal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempBasal.swift; sourceTree = "<group>"; };
 		38A13D3125E28B4B00EAA382 /* PumpHistoryEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpHistoryEvent.swift; sourceTree = "<group>"; };
@@ -533,6 +542,9 @@
 		8782B44544F38F2B2D82C38E /* NightscoutConfigRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NightscoutConfigRootView.swift; sourceTree = "<group>"; };
 		8A965332F237348B119FB858 /* PreferencesEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PreferencesEditorRootView.swift; sourceTree = "<group>"; };
 		8C3B5FD881CA45DFDEE0EDA9 /* AddTempTargetViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddTempTargetViewModel.swift; sourceTree = "<group>"; };
+		8CF5ACEE1F0859670E71B2C0 /* AutotuneConfigRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AutotuneConfigRootView.swift; sourceTree = "<group>"; };
+		8DCCCCE633F5E98E41B0CD3C /* AutotuneConfigDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AutotuneConfigDataFlow.swift; sourceTree = "<group>"; };
+		91B260840169E712C05ACC1F /* AutotuneConfigBuilder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AutotuneConfigBuilder.swift; sourceTree = "<group>"; };
 		920DDB21E5D0EB813197500D /* ConfigEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConfigEditorRootView.swift; sourceTree = "<group>"; };
 		96653287EDB276A111288305 /* ManualTempBasalDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalDataFlow.swift; sourceTree = "<group>"; };
 		9C8D5F457B5AFF763F8CF3DF /* CREditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CREditorProvider.swift; sourceTree = "<group>"; };
@@ -543,6 +555,7 @@
 		AAFF91130F2FCCC7EBBA11AD /* BasalProfileEditorViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorViewModel.swift; sourceTree = "<group>"; };
 		AEE53A13D26F101B332EFFC8 /* AddTempTargetProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddTempTargetProvider.swift; sourceTree = "<group>"; };
 		AF65DA88F972B56090AD6AC3 /* PumpConfigDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpConfigDataFlow.swift; sourceTree = "<group>"; };
+		B5EF98E22A39CD656A230704 /* AutotuneConfigProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AutotuneConfigProvider.swift; sourceTree = "<group>"; };
 		B8C7F882606FF83A21BE00D8 /* PumpSettingsEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpSettingsEditorRootView.swift; sourceTree = "<group>"; };
 		BA49538D56989D8DA6FCF538 /* TargetsEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorDataFlow.swift; sourceTree = "<group>"; };
 		BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorRootView.swift; sourceTree = "<group>"; };
@@ -551,6 +564,7 @@
 		C8D1A7CA8C10C4403D4BBFA7 /* BolusDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BolusDataFlow.swift; sourceTree = "<group>"; };
 		CFCFE0781F9074C2917890E8 /* ManualTempBasalViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalViewModel.swift; sourceTree = "<group>"; };
 		D0BDC6993C1087310EDFC428 /* CREditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CREditorRootView.swift; sourceTree = "<group>"; };
+		D295A3F870E826BE371C0BB5 /* AutotuneConfigViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AutotuneConfigViewModel.swift; sourceTree = "<group>"; };
 		D97F14812C1AFED3621165A5 /* PumpSettingsEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpSettingsEditorProvider.swift; sourceTree = "<group>"; };
 		E01C416A0792696C6911C1D7 /* PumpConfigBuilder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpConfigBuilder.swift; sourceTree = "<group>"; };
 		E08D9D69E5B052E5C9E8BD32 /* PreferencesEditorBuilder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PreferencesEditorBuilder.swift; sourceTree = "<group>"; };
@@ -633,6 +647,7 @@
 				6DC5D590658EF8B8DF94F9F5 /* AddCarbs */,
 				A9A4C88374496B3C89058A89 /* AddTempTarget */,
 				3811DE4525C9D4B800A708ED /* AuthotizedRoot */,
+				672F63EEAE27400625E14BAD /* AutotuneConfig */,
 				A42F1FEDFFD0DDE00AAD54D3 /* BasalProfileEditor */,
 				3811DE0425C9D32E00A708ED /* Base */,
 				C2C98283C436DB934D7E7994 /* Bolus */,
@@ -1023,6 +1038,7 @@
 			children = (
 				385CEAC025F2EA52002D6D5B /* Announcement.swift */,
 				388E5A5F25B6F2310019842D /* Autosens.swift */,
+				38A00B1E25FC00F7006BC0B0 /* Autotune.swift */,
 				388358C725EEF6D200E024B2 /* BasalProfileEntry.swift */,
 				38D0B3B525EBE24900CB6E88 /* Battery.swift */,
 				382C134A25F14E3700715CE1 /* BGTargets.swift */,
@@ -1035,6 +1051,7 @@
 				382C133625F13A1E00715CE1 /* InsulinSensitivities.swift */,
 				38887CCD25F5725200944304 /* IOBEntry.swift */,
 				385CEA8125F23DFD002D6D5B /* NightscoutStatus.swift */,
+				389442CA25F65F7100FA1F27 /* NightscoutTreatment.swift */,
 				3895E4C525B9E00D00214B37 /* Preferences.swift */,
 				38A13D3125E28B4B00EAA382 /* PumpHistoryEvent.swift */,
 				3883583325EEB38000E024B2 /* PumpSettings.swift */,
@@ -1044,7 +1061,6 @@
 				38A0364125ED069400FCBB52 /* TempBasal.swift */,
 				3871F39B25ED892B0013ECB5 /* TempTarget.swift */,
 				3811DE8E25C9D80400A708ED /* User.swift */,
-				389442CA25F65F7100FA1F27 /* NightscoutTreatment.swift */,
 			);
 			path = Models;
 			sourceTree = "<group>";
@@ -1052,6 +1068,7 @@
 		388E5A5A25B6F05F0019842D /* Helpers */ = {
 			isa = PBXGroup;
 			children = (
+				38A00B2225FC2B55006BC0B0 /* LRUCache.swift */,
 				3871F39E25ED895A0013ECB5 /* Decimal+Extensions.swift */,
 				38C4D33625E9A1A200D30B77 /* DispatchQueue+Extensions.swift */,
 				3811DE5425C9D4D500A708ED /* Formatters.swift */,
@@ -1251,6 +1268,14 @@
 			path = View;
 			sourceTree = "<group>";
 		};
+		55DE731ACE8289FAF3819077 /* View */ = {
+			isa = PBXGroup;
+			children = (
+				8CF5ACEE1F0859670E71B2C0 /* AutotuneConfigRootView.swift */,
+			);
+			path = View;
+			sourceTree = "<group>";
+		};
 		64271A287C92581EADCB47FA /* View */ = {
 			isa = PBXGroup;
 			children = (
@@ -1368,6 +1393,18 @@
 			path = Helpers;
 			sourceTree = "<group>";
 		};
+		672F63EEAE27400625E14BAD /* AutotuneConfig */ = {
+			isa = PBXGroup;
+			children = (
+				91B260840169E712C05ACC1F /* AutotuneConfigBuilder.swift */,
+				8DCCCCE633F5E98E41B0CD3C /* AutotuneConfigDataFlow.swift */,
+				B5EF98E22A39CD656A230704 /* AutotuneConfigProvider.swift */,
+				D295A3F870E826BE371C0BB5 /* AutotuneConfigViewModel.swift */,
+				55DE731ACE8289FAF3819077 /* View */,
+			);
+			path = AutotuneConfig;
+			sourceTree = "<group>";
+		};
 		6DC5D590658EF8B8DF94F9F5 /* AddCarbs */ = {
 			isa = PBXGroup;
 			children = (
@@ -1852,6 +1889,7 @@
 				5BFA1C2208114643B77F8CEB /* AddTempTargetProvider.swift in Sources */,
 				919DBD08F13BAFB180DF6F47 /* AddTempTargetViewModel.swift in Sources */,
 				8BC2F5A29AD1ED08AC0EE013 /* AddTempTargetRootView.swift in Sources */,
+				38A00B1F25FC00F7006BC0B0 /* Autotune.swift in Sources */,
 				19434C14DF3F4816F4E4BF2E /* BolusBuilder.swift in Sources */,
 				041D1E995A6AE92E9289DC49 /* BolusDataFlow.swift in Sources */,
 				6610FA1E25FAED29004781D7 /* MeshEntryOrientations.swift in Sources */,
@@ -1864,6 +1902,12 @@
 				BF1667ADE69E4B5B111CECAE /* ManualTempBasalProvider.swift in Sources */,
 				C967DACD3B1E638F8B43BE06 /* ManualTempBasalViewModel.swift in Sources */,
 				7BCFACB97C821041BA43A114 /* ManualTempBasalRootView.swift in Sources */,
+				38A00B2325FC2B55006BC0B0 /* LRUCache.swift in Sources */,
+				91732A8060347C0E67024D80 /* AutotuneConfigBuilder.swift in Sources */,
+				3083261C4B268E353F36CD0B /* AutotuneConfigDataFlow.swift in Sources */,
+				891DECF7BC20968D7F566161 /* AutotuneConfigProvider.swift in Sources */,
+				D76333C9256787610B3B4875 /* AutotuneConfigViewModel.swift in Sources */,
+				A05235B9112E677ED03B6E8E /* AutotuneConfigRootView.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};

+ 3 - 2
FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json

@@ -1,5 +1,6 @@
 {
     "units": "mmol/L",
-    "closedLoop": false
-    "allowAnnouncements": false
+    "closedLoop": false,
+    "allowAnnouncements": false,
+    "useAutotune": false
 }

+ 22 - 3
FreeAPS/Sources/APS/APSManager.swift

@@ -2,11 +2,12 @@ import Combine
 import Foundation
 import LoopKit
 import LoopKitUI
+import SwiftDate
 import Swinject
 
 protocol APSManager {
     func fetchAndLoop()
-    func autotune()
+    func autotune() -> AnyPublisher<Autotune?, Never>
     func enactBolus(amount: Double)
     var pumpManager: PumpManagerUI? { get set }
     var pumpDisplayState: CurrentValueSubject<PumpDisplayState?, Never> { get }
@@ -25,6 +26,8 @@ final class BaseAPSManager: APSManager, Injectable {
     @Injected() private var nightscout: NightscoutManager!
     @Injected() private var settingsManager: SettingsManager!
     @Injected() private var broadcaster: Broadcaster!
+    @Persisted(key: "lastAutotuneDate") private var lastAutotuneDate: Date = .distantPast
+
     private var openAPS: OpenAPS!
 
     private var lifetime = Set<AnyCancellable>()
@@ -84,6 +87,7 @@ final class BaseAPSManager: APSManager, Injectable {
             nightscout.fetchCarbs(),
             nightscout.fetchTempTargets()
         )
+        .flatMap { _ in self.daylyAutotune() }
         .flatMap { _ in self.autosens() }
         .flatMap { _ in self.determineBasal() }
         .sink { _ in } receiveValue: { [weak self] ok in
@@ -181,8 +185,23 @@ final class BaseAPSManager: APSManager, Injectable {
         }
     }
 
-    func autotune() {
-        openAPS.autotune().sink {}.store(in: &lifetime)
+    func daylyAutotune() -> AnyPublisher<Bool, Never> {
+        guard settings.useAutotune else {
+            return Just(false).eraseToAnyPublisher()
+        }
+
+        let now = Date()
+
+        guard lastAutotuneDate.isBeforeDate(now, granularity: .day) else {
+            return Just(false).eraseToAnyPublisher()
+        }
+        lastAutotuneDate = now
+
+        return autotune().map { $0 != nil }.eraseToAnyPublisher()
+    }
+
+    func autotune() -> AnyPublisher<Autotune?, Never> {
+        openAPS.autotune().eraseToAnyPublisher()
     }
 
     private func enactAnnouncement(_ announcement: Announcement) {

+ 8 - 4
FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift

@@ -107,7 +107,7 @@ final class OpenAPS {
         }
     }
 
-    func autotune(categorizeUamAsBasal: Bool = false, tuneInsulinCurve: Bool = false) -> Future<Void, Never> {
+    func autotune(categorizeUamAsBasal: Bool = false, tuneInsulinCurve: Bool = false) -> Future<Autotune?, Never> {
         Future { promise in
             self.processQueue.async {
                 let pumpHistory = self.loadFileFromStorage(name: OpenAPS.Monitor.pumpHistory)
@@ -132,10 +132,14 @@ final class OpenAPS {
                     pumpProfile: profile
                 )
 
-                try? self.storage.save(autotuneResult, as: Settings.autotune)
-
                 debug(.openAPS, "AUTOTUNE RESULT: \(autotuneResult)")
-                promise(.success(()))
+
+                if let autotune = Autotune(from: autotuneResult) {
+                    try? self.storage.save(autotuneResult, as: Settings.autotune)
+                    promise(.success(autotune))
+                } else {
+                    promise(.success(nil))
+                }
             }
         }
     }

+ 106 - 0
FreeAPS/Sources/Helpers/LRUCache.swift

@@ -0,0 +1,106 @@
+import Foundation
+
+private class List<Key>: CustomDebugStringConvertible {
+    var debugDescription: String { "\(value)" }
+
+    var value: Key
+    var prev: List?
+    var next: List?
+
+    init(_ val: Key) {
+        value = val
+    }
+}
+
+final class LRUCache<Key, Value> where Key: Hashable {
+    private var cache: [Key: Value] = [:]
+    private var listBegin: List<Key>?
+    private var listEnd: List<Key>?
+    private var listCache: [Key: List<Key>] = [:]
+    private let lock = NSRecursiveLock()
+    private let capacity: Int
+
+    init(capacity: Int) {
+        cache.reserveCapacity(capacity)
+        listCache.reserveCapacity(capacity)
+        self.capacity = capacity
+    }
+
+    var isEmpty: Bool {
+        lock.perform {
+            cache.isEmpty
+        }
+    }
+
+    var isFull: Bool {
+        lock.perform {
+            cache.count == capacity
+        }
+    }
+
+    var count: Int {
+        lock.perform {
+            cache.count
+        }
+    }
+
+    var allValues: [Value] {
+        lock.perform {
+            Array(cache.values)
+        }
+    }
+
+    func removeAll() {
+        listCache.keys.forEach(remove(_:))
+    }
+
+    subscript(key: Key) -> Value? {
+        get {
+            lock.perform {
+                guard let value = cache[key] else { return nil }
+                self[key] = value
+                return value
+            }
+        }
+        set {
+            lock.perform {
+                remove(key)
+                if let value = newValue {
+                    insert(key, value)
+                }
+            }
+        }
+    }
+
+    private func remove(_ key: Key) {
+        autoreleasepool {
+            guard let node = listCache[key] else { return }
+            listCache[key] = nil
+            cache[key] = nil
+            let p = node.prev
+            let n = node.next
+
+            p?.next = n
+            n?.prev = p
+            if node.value == listBegin!.value {
+                listBegin = n
+            }
+            if node.value == listEnd!.value {
+                listEnd = listEnd!.prev
+            }
+        }
+    }
+
+    private func insert(_ key: Key, _ value: Value) {
+        if cache.count == capacity {
+            remove(listBegin!.value)
+        }
+        let le = listEnd
+        listEnd = List(key)
+        le?.next = listEnd
+        listEnd!.prev = le
+        listBegin = listBegin ?? listEnd
+        listCache[key] = listEnd
+        cache[key] = value
+    }
+}

+ 17 - 0
FreeAPS/Sources/Models/Autotune.swift

@@ -0,0 +1,17 @@
+import Foundation
+
+struct Autotune: JSON {
+    var createdAt: Date?
+    let basalProfile: [BasalProfileEntry]
+    let sensitivity: Decimal
+    let carbRatio: Decimal
+}
+
+extension Autotune {
+    private enum CodingKeys: String, CodingKey {
+        case createdAt = "created_at"
+        case basalProfile = "basalprofile"
+        case sensitivity = "sens"
+        case carbRatio = "carb_ratio"
+    }
+}

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

@@ -4,4 +4,5 @@ struct FreeAPSSettings: JSON {
     var units: GlucoseUnits
     var closedLoop: Bool
     var allowAnnouncements: Bool
+    var useAutotune: Bool
 }

+ 3 - 0
FreeAPS/Sources/Modules/AutotuneConfig/AutotuneConfigBuilder.swift

@@ -0,0 +1,3 @@
+extension AutotuneConfig {
+    final class Builder: BaseModuleBuilder<RootView, ViewModel<Provider>, Provider> {}
+}

+ 11 - 0
FreeAPS/Sources/Modules/AutotuneConfig/AutotuneConfigDataFlow.swift

@@ -0,0 +1,11 @@
+import Combine
+
+enum AutotuneConfig {
+    enum Config {}
+}
+
+protocol AutotuneConfigProvider: Provider {
+    var autotune: Autotune? { get }
+    func runAutotune() -> AnyPublisher<Autotune?, Never>
+    func deleteAutotune()
+}

+ 19 - 0
FreeAPS/Sources/Modules/AutotuneConfig/AutotuneConfigProvider.swift

@@ -0,0 +1,19 @@
+import Combine
+
+extension AutotuneConfig {
+    final class Provider: BaseProvider, AutotuneConfigProvider {
+        @Injected() private var apsManager: APSManager!
+
+        var autotune: Autotune? {
+            try? storage.retrieve(OpenAPS.Settings.autotune, as: Autotune.self)
+        }
+
+        func runAutotune() -> AnyPublisher<Autotune?, Never> {
+            apsManager.autotune()
+        }
+
+        func deleteAutotune() {
+            try? storage.remove(OpenAPS.Settings.autotune)
+        }
+    }
+}

+ 36 - 0
FreeAPS/Sources/Modules/AutotuneConfig/AutotuneConfigViewModel.swift

@@ -0,0 +1,36 @@
+import SwiftUI
+
+extension AutotuneConfig {
+    class ViewModel<Provider>: BaseViewModel<Provider>, ObservableObject where Provider: AutotuneConfigProvider {
+        @Injected() var settingsManager: SettingsManager!
+        @Published var useAutotune = false
+        @Published var autotune: Autotune?
+        private(set) var units: GlucoseUnits = .mmolL
+
+        override func subscribe() {
+            autotune = provider.autotune
+            units = settingsManager.settings.units
+            useAutotune = settingsManager.settings.useAutotune
+
+            $useAutotune
+                .removeDuplicates()
+                .sink { [weak self] use in
+                    self?.settingsManager.settings.useAutotune = use
+                }
+                .store(in: &lifetime)
+        }
+
+        func run() {
+            provider.runAutotune()
+                .receive(on: DispatchQueue.main)
+                .sink { [weak self] result in
+                    self?.autotune = result
+                }.store(in: &lifetime)
+        }
+
+        func delete() {
+            provider.deleteAutotune()
+            autotune = nil
+        }
+    }
+}

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

@@ -0,0 +1,74 @@
+import SwiftUI
+
+extension AutotuneConfig {
+    struct RootView: BaseView {
+        @EnvironmentObject var viewModel: ViewModel<Provider>
+
+        private var isfFormatter: NumberFormatter {
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            formatter.maximumFractionDigits = 1
+            return formatter
+        }
+
+        private var rateFormatter: NumberFormatter {
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            formatter.maximumFractionDigits = 3
+            return formatter
+        }
+
+        var body: some View {
+            Form {
+                Section {
+                    Toggle("Use Autotune", isOn: $viewModel.useAutotune)
+                }
+
+                Section {
+                    Button { viewModel.run() }
+                    label: { Text("Run now") }
+                }
+
+                if let autotune = viewModel.autotune {
+                    Section {
+                        HStack {
+                            Text("Carb ratio")
+                            Spacer()
+                            Text(isfFormatter.string(from: autotune.carbRatio as NSNumber) ?? "0")
+                            Text("g/U").foregroundColor(.secondary)
+                        }
+                        HStack {
+                            Text("Sensitivity")
+                            Spacer()
+                            if viewModel.units == .mmolL {
+                                Text(isfFormatter.string(from: autotune.sensitivity.asMmolL as NSNumber) ?? "0")
+                            } else {
+                                Text(isfFormatter.string(from: autotune.sensitivity as NSNumber) ?? "0")
+                            }
+                            Text(viewModel.units.rawValue + "/U").foregroundColor(.secondary)
+                        }
+                    }
+
+                    Section(header: Text("Basal profile")) {
+                        ForEach(0 ..< autotune.basalProfile.count, id: \.self) { index in
+                            HStack {
+                                Text(autotune.basalProfile[index].start).foregroundColor(.secondary)
+                                Spacer()
+                                Text(rateFormatter.string(from: autotune.basalProfile[index].rate as NSNumber) ?? "0")
+                                Text("U/h").foregroundColor(.secondary)
+                            }
+                        }
+                    }
+
+                    Section {
+                        Button { viewModel.delete() }
+                        label: { Text("Delete autotune data") }
+                            .foregroundColor(.red)
+                    }
+                }
+            }
+            .navigationTitle("Autotune")
+            .navigationBarTitleDisplayMode(.automatic)
+        }
+    }
+}

+ 0 - 24
FreeAPS/Sources/Modules/Base/BaseProvider.swift

@@ -4,11 +4,9 @@ import Swinject
 
 protocol Provider {
     init(resolver: Resolver)
-    var user: CurrentValueSubject<User?, Never> { get }
 }
 
 class BaseProvider: Provider, Injectable {
-    let user = CurrentValueSubject<User?, Never>(nil)
     var lifetime = Set<AnyCancellable>()
     @Injected() var authorizationManager: AuthorizationManager!
     @Injected() var deviceManager: DeviceDataManager!
@@ -16,27 +14,5 @@ class BaseProvider: Provider, Injectable {
 
     required init(resolver: Resolver) {
         injectServices(resolver)
-        subscribe()
-    }
-
-    private func subscribe() {
-        authorizationManager.credentials
-            .map { credentials -> User? in
-                guard let credentials = credentials else { return nil }
-                return User(id: credentials.id)
-            }
-            .sink { user in
-                self.user.send(user)
-            }
-            .store(in: &lifetime)
-    }
-
-    func type(for file: String) -> JSON.Type {
-        switch file {
-        case OpenAPS.Monitor.pumpHistory:
-            return [PumpHistoryEvent].self
-        default:
-            return RawJSON.self
-        }
     }
 }

+ 3 - 0
FreeAPS/Sources/Modules/ISFEditor/ISFEditorViewModel.swift

@@ -5,6 +5,7 @@ extension ISFEditor {
         @Injected() var settingsManager: SettingsManager!
         @Published var items: [Item] = []
         private(set) var autosensISF: Double?
+        private(set) var autosensRatio: Double = 0
 
         let timeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
 
@@ -41,6 +42,8 @@ extension ISFEditor {
                     autosensISF = round(Double(newISF * GlucoseUnits.exchangeRate) * 10) / 10
                 }
             }
+
+            autosensRatio = Double(provider.autosense.ratio)
         }
 
         func add() {

+ 6 - 1
FreeAPS/Sources/Modules/ISFEditor/View/ISFEditorRootView.swift

@@ -15,7 +15,7 @@ extension ISFEditor {
         private var rateFormatter: NumberFormatter {
             let formatter = NumberFormatter()
             formatter.numberStyle = .decimal
-            formatter.maximumFractionDigits = 1
+            formatter.maximumFractionDigits = 2
             return formatter
         }
 
@@ -24,6 +24,11 @@ extension ISFEditor {
                 if let newISF = viewModel.autosensISF {
                     Section(header: Text("Autosens")) {
                         HStack {
+                            Text("Sensitivity Ratio").foregroundColor(.secondary)
+                            Spacer()
+                            Text(rateFormatter.string(from: viewModel.autosensRatio as NSNumber) ?? "1")
+                        }
+                        HStack {
                             Text("Calculated ISF").foregroundColor(.secondary)
                             Spacer()
                             Text((rateFormatter.string(from: newISF as NSNumber) ?? "0") + " \(viewModel.units.rawValue)/U")

+ 2 - 0
FreeAPS/Sources/Modules/PreferencesEditor/PreferencesEditorViewModel.swift

@@ -26,12 +26,14 @@ extension PreferencesEditor {
             insulinCirveField.settable = self
 
             $unitsIndex
+                .removeDuplicates()
                 .sink { [weak self] index in
                     self?.settingsManager.settings.units = index == 0 ? .mgdL : .mmolL
                 }
                 .store(in: &lifetime)
 
             $allowAnnouncements
+                .removeDuplicates()
                 .sink { [weak self] allow in
                     self?.settingsManager.settings.allowAnnouncements = allow
                 }

+ 3 - 0
FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift

@@ -25,6 +25,7 @@ extension Settings {
                     Text("Carb Ratios").chevronCell().navigationLink(to: .crEditor, from: self)
                     Text("Target Ranges").chevronCell().navigationLink(to: .targetsEditor, from: self)
                     Text("Preferences").chevronCell().navigationLink(to: .preferencesEditor, from: self)
+                    Text("Autotune").chevronCell().navigationLink(to: .autotuneConfig, from: self)
                 }
 
                 Section(header: Text("Config files")) {
@@ -67,6 +68,8 @@ extension Settings {
                             .navigationLink(to: .configEditor(file: OpenAPS.FreeAPS.announcements), from: self)
                         Text("Enacted announcements").chevronCell()
                             .navigationLink(to: .configEditor(file: OpenAPS.FreeAPS.announcementsEnacted), from: self)
+                        Text("Autotune").chevronCell()
+                            .navigationLink(to: .configEditor(file: OpenAPS.Settings.autotune), from: self)
                     }
                 }
             }

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

@@ -21,6 +21,7 @@ enum Screen: Identifiable {
     case addTempTarget
     case bolus
     case manualTempBasal
+    case autotuneConfig
 
     var id: Int { String(reflecting: self).hashValue }
 }
@@ -66,6 +67,8 @@ extension Screen {
             return Bolus.Builder(resolver: resolver).buildView()
         case .manualTempBasal:
             return ManualTempBasal.Builder(resolver: resolver).buildView()
+        case .autotuneConfig:
+            return AutotuneConfig.Builder(resolver: resolver).buildView()
         }
     }
 

+ 0 - 1
FreeAPS/Sources/Services/AuthorizationManager/AuthorizationManager.swift

@@ -5,7 +5,6 @@ import Swinject
 protocol AuthorizationManager {
     var isAuthorized: Bool { get }
     var authorizationPublisher: AnyPublisher<Bool, Never> { get }
-    var credentials: CurrentValueSubject<Credentials?, Never> { get }
     func authorize(credentials: Credentials) -> AnyPublisher<Void, Never>
     func logout()
 }

+ 1 - 1
FreeAPS/Sources/Services/SettingsManager/SettingsManager.swift

@@ -29,7 +29,7 @@ final class BaseSettingsManager: SettingsManager, Injectable {
         let storage = resolver.resolve(FileStorage.self)!
         settings = (try? storage.retrieve(OpenAPS.FreeAPS.settings, as: FreeAPSSettings.self))
             ?? FreeAPSSettings(from: OpenAPS.defaults(for: OpenAPS.FreeAPS.settings))
-            ?? FreeAPSSettings(units: .mmolL, closedLoop: false, allowAnnouncements: false)
+            ?? FreeAPSSettings(units: .mmolL, closedLoop: false, allowAnnouncements: false, useAutotune: false)
 
         injectServices(resolver)
     }

+ 22 - 8
FreeAPS/Sources/Views/ViewModifiers.swift

@@ -37,26 +37,40 @@ struct CapsulaBackground: ViewModifier {
     }
 }
 
-struct NavigationLazyView<Content: View>: View {
-    let build: () -> Content
-    init(_ build: @autoclosure @escaping () -> Content) {
+private let navigationCache = LRUCache<Screen.ID, AnyView>(capacity: 10)
+
+struct NavigationLazyView: View {
+    let build: () -> AnyView
+    let screen: Screen
+
+    init(_ build: @autoclosure @escaping () -> AnyView, screen: Screen) {
         self.build = build
+        self.screen = screen
     }
 
-    var body: Content {
-        build()
+    var body: AnyView {
+        if navigationCache[screen.id] == nil {
+            navigationCache[screen.id] = build()
+        }
+        return navigationCache[screen.id]!
+            .onDisappear {
+                navigationCache[screen.id] = nil
+            }.asAny()
     }
 }
 
 struct Link<T>: ViewModifier where T: View {
     private let destination: () -> T
-    init(destination: @autoclosure @escaping () -> T) {
+    let screen: Screen
+
+    init(destination: @autoclosure @escaping () -> T, screen: Screen) {
         self.destination = destination
+        self.screen = screen
     }
 
     func body(content: Content) -> some View {
         ZStack {
-            NavigationLink(destination: NavigationLazyView(destination())) {
+            NavigationLink(destination: NavigationLazyView(destination().asAny(), screen: screen)) {
                 EmptyView()
             }.hidden()
             content
@@ -130,7 +144,7 @@ extension View {
     }
 
     func navigationLink<V: BaseView>(to screen: Screen, from view: V) -> some View {
-        modifier(Link(destination: view.viewModel.view(for: screen)))
+        modifier(Link(destination: view.viewModel.view(for: screen), screen: screen))
     }
 
     func adaptsToSoftwareKeyboard() -> some View {