Explorar el Código

Insulin Sensitivities editor

Ivan Valkou hace 5 años
padre
commit
d2580d6689

+ 4 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -83,6 +83,7 @@
 		3811DF0825CAAA4700A708ED /* ServiceContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3811DF0725CAAA4700A708ED /* ServiceContainer.swift */; };
 		3811DF0825CAAA4700A708ED /* ServiceContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3811DF0725CAAA4700A708ED /* ServiceContainer.swift */; };
 		3811DF1025CAAAE200A708ED /* APSManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3811DF0F25CAAAE200A708ED /* APSManager.swift */; };
 		3811DF1025CAAAE200A708ED /* APSManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3811DF0F25CAAAE200A708ED /* APSManager.swift */; };
 		3821ED4C25DD18BA00BC42AD /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3821ED4B25DD18BA00BC42AD /* Constants.swift */; };
 		3821ED4C25DD18BA00BC42AD /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3821ED4B25DD18BA00BC42AD /* Constants.swift */; };
+		382C133725F13A1E00715CE1 /* InsulinSensitivities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 382C133625F13A1E00715CE1 /* InsulinSensitivities.swift */; };
 		383948D325CD4D6D00E91849 /* Disk in Frameworks */ = {isa = PBXBuildFile; productRef = 383948D225CD4D6D00E91849 /* Disk */; };
 		383948D325CD4D6D00E91849 /* Disk in Frameworks */ = {isa = PBXBuildFile; productRef = 383948D225CD4D6D00E91849 /* Disk */; };
 		383948D625CD4D8900E91849 /* FileStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 383948D525CD4D8900E91849 /* FileStorage.swift */; };
 		383948D625CD4D8900E91849 /* FileStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 383948D525CD4D8900E91849 /* FileStorage.swift */; };
 		383948DA25CD64D500E91849 /* Glucose.swift in Sources */ = {isa = PBXBuildFile; fileRef = 383948D925CD64D500E91849 /* Glucose.swift */; };
 		383948DA25CD64D500E91849 /* Glucose.swift in Sources */ = {isa = PBXBuildFile; fileRef = 383948D925CD64D500E91849 /* Glucose.swift */; };
@@ -664,6 +665,7 @@
 		3811DF0725CAAA4700A708ED /* ServiceContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceContainer.swift; sourceTree = "<group>"; };
 		3811DF0725CAAA4700A708ED /* ServiceContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceContainer.swift; sourceTree = "<group>"; };
 		3811DF0F25CAAAE200A708ED /* APSManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APSManager.swift; sourceTree = "<group>"; };
 		3811DF0F25CAAAE200A708ED /* APSManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APSManager.swift; sourceTree = "<group>"; };
 		3821ED4B25DD18BA00BC42AD /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
 		3821ED4B25DD18BA00BC42AD /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
+		382C133625F13A1E00715CE1 /* InsulinSensitivities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsulinSensitivities.swift; sourceTree = "<group>"; };
 		383948D525CD4D8900E91849 /* FileStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileStorage.swift; sourceTree = "<group>"; };
 		383948D525CD4D8900E91849 /* FileStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileStorage.swift; sourceTree = "<group>"; };
 		383948D925CD64D500E91849 /* Glucose.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Glucose.swift; sourceTree = "<group>"; };
 		383948D925CD64D500E91849 /* Glucose.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Glucose.swift; sourceTree = "<group>"; };
 		384E803325C385E60086DB71 /* JavaScriptWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JavaScriptWorker.swift; sourceTree = "<group>"; };
 		384E803325C385E60086DB71 /* JavaScriptWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JavaScriptWorker.swift; sourceTree = "<group>"; };
@@ -1228,6 +1230,7 @@
 				38A0364125ED069400FCBB52 /* TempBasal.swift */,
 				38A0364125ED069400FCBB52 /* TempBasal.swift */,
 				3871F39B25ED892B0013ECB5 /* TempTarget.swift */,
 				3871F39B25ED892B0013ECB5 /* TempTarget.swift */,
 				3811DE8E25C9D80400A708ED /* User.swift */,
 				3811DE8E25C9D80400A708ED /* User.swift */,
+				382C133625F13A1E00715CE1 /* InsulinSensitivities.swift */,
 			);
 			);
 			path = Models;
 			path = Models;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -1961,6 +1964,7 @@
 				3811DE0925C9D32F00A708ED /* BaseViewModel.swift in Sources */,
 				3811DE0925C9D32F00A708ED /* BaseViewModel.swift in Sources */,
 				3883583425EEB38000E024B2 /* PumpSettings.swift in Sources */,
 				3883583425EEB38000E024B2 /* PumpSettings.swift in Sources */,
 				3811DEB125C9D88300A708ED /* Keychain.swift in Sources */,
 				3811DEB125C9D88300A708ED /* Keychain.swift in Sources */,
+				382C133725F13A1E00715CE1 /* InsulinSensitivities.swift in Sources */,
 				3811DE7B25C9D6D300A708ED /* LoginProvider.swift in Sources */,
 				3811DE7B25C9D6D300A708ED /* LoginProvider.swift in Sources */,
 				383948D625CD4D8900E91849 /* FileStorage.swift in Sources */,
 				383948D625CD4D8900E91849 /* FileStorage.swift in Sources */,
 				3811DE4125C9D4A100A708ED /* SettingsRootView.swift in Sources */,
 				3811DE4125C9D4A100A708ED /* SettingsRootView.swift in Sources */,

+ 2 - 2
FreeAPS/Resources/json/defaults/settings/insulin_sensitivities.json

@@ -1,9 +1,9 @@
 {
 {
-    "units": "mg/dL",
+    "units": "mmol/L",
     "user_preferred_units": "mmol/L",
     "user_preferred_units": "mmol/L",
     "sensitivities": [
     "sensitivities": [
         {
         {
-            "sensitivity": 72.0,
+            "sensitivity": 3.0,
             "offset": 0,
             "offset": 0,
             "start": "00:00:00"
             "start": "00:00:00"
         }
         }

+ 6 - 0
FreeAPS/Sources/Helpers/Decimal+Extensions.swift

@@ -5,3 +5,9 @@ extension Double {
         self.init(truncating: decimal as NSNumber)
         self.init(truncating: decimal as NSNumber)
     }
     }
 }
 }
+
+extension Int {
+    init(_ decimal: Decimal) {
+        self.init(Double(decimal))
+    }
+}

+ 1 - 1
FreeAPS/Sources/Helpers/JSON.swift

@@ -77,7 +77,7 @@ extension Dictionary where Key == String {
 enum JSONCoding {
 enum JSONCoding {
     static var encoder: JSONEncoder {
     static var encoder: JSONEncoder {
         let encoder = JSONEncoder()
         let encoder = JSONEncoder()
-        encoder.outputFormatting = .prettyPrinted
+        encoder.outputFormatting = [.prettyPrinted, .withoutEscapingSlashes]
         encoder.dateEncodingStrategy = .customISO8601
         encoder.dateEncodingStrategy = .customISO8601
         return encoder
         return encoder
     }
     }

+ 3 - 1
FreeAPS/Sources/Models/BloodGlucose.swift

@@ -28,7 +28,9 @@ struct BloodGlucose: JSON {
     var isStateValid: Bool { sgv ?? 0 >= 39 && noise ?? 1 != 4 }
     var isStateValid: Bool { sgv ?? 0 >= 39 && noise ?? 1 != 4 }
 }
 }
 
 
-enum GlucoseUnit: String, JSON {
+enum GlucoseUnits: String, JSON {
     case mgdL = "mg/dL"
     case mgdL = "mg/dL"
     case mmolL = "mmol/L"
     case mmolL = "mmol/L"
+
+    static let exchangeRate: Decimal = 0.0555
 }
 }

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

@@ -1,6 +1,6 @@
 import Foundation
 import Foundation
 
 
 struct FreeAPSSettings: JSON {
 struct FreeAPSSettings: JSON {
-    var units: GlucoseUnit
+    var units: GlucoseUnits
     var closedLoop: Bool
     var closedLoop: Bool
 }
 }

+ 21 - 0
FreeAPS/Sources/Models/InsulinSensitivities.swift

@@ -0,0 +1,21 @@
+import Foundation
+
+struct InsulinSensitivities: JSON {
+    let units: GlucoseUnits
+    let userPrefferedUnits: GlucoseUnits
+    let sensitivities: [InsulinSensitivityEntry]
+}
+
+extension InsulinSensitivities {
+    private enum CodingKeys: String, CodingKey {
+        case units
+        case userPrefferedUnits = "user_preferred_units"
+        case sensitivities
+    }
+}
+
+struct InsulinSensitivityEntry: JSON {
+    let sensitivity: Decimal
+    let offset: Int
+    let start: String
+}

+ 25 - 1
FreeAPS/Sources/Modules/ISFEditor/ISFEditorDataFlow.swift

@@ -1,5 +1,29 @@
+import Foundation
+
 enum ISFEditor {
 enum ISFEditor {
     enum Config {}
     enum Config {}
+
+    class Item: Identifiable, Hashable, Equatable {
+        let id = UUID()
+        var rateIndex = 0
+        var timeIndex = 0
+
+        init(rateIndex: Int, selectedIndex: Int) {
+            self.rateIndex = rateIndex
+            timeIndex = selectedIndex
+        }
+
+        static func == (lhs: Item, rhs: Item) -> Bool {
+            lhs.timeIndex == rhs.timeIndex
+        }
+
+        func hash(into hasher: inout Hasher) {
+            hasher.combine(timeIndex)
+        }
+    }
 }
 }
 
 
-protocol ISFEditorProvider: Provider {}
+protocol ISFEditorProvider: Provider {
+    var profile: InsulinSensitivities { get }
+    func saveProfile(_ profile: InsulinSensitivities)
+}

+ 15 - 1
FreeAPS/Sources/Modules/ISFEditor/ISFEditorProvider.swift

@@ -1,3 +1,17 @@
 extension ISFEditor {
 extension ISFEditor {
-    final class Provider: BaseProvider, ISFEditorProvider {}
+    final class Provider: BaseProvider, ISFEditorProvider {
+        var profile: InsulinSensitivities {
+            (try? storage.retrieve(OpenAPS.Settings.insulinSensitivities, as: InsulinSensitivities.self))
+                ?? InsulinSensitivities(from: OpenAPS.defaults(for: OpenAPS.Settings.insulinSensitivities))
+                ?? InsulinSensitivities(
+                    units: .mmolL,
+                    userPrefferedUnits: .mmolL,
+                    sensitivities: [InsulinSensitivityEntry(sensitivity: 3.0, offset: 0, start: "00:00:00")]
+                )
+        }
+
+        func saveProfile(_ profile: InsulinSensitivities) {
+            try? storage.save(profile, as: OpenAPS.Settings.insulinSensitivities)
+        }
+    }
 }
 }

+ 66 - 1
FreeAPS/Sources/Modules/ISFEditor/ISFEditorViewModel.swift

@@ -2,6 +2,71 @@ import SwiftUI
 
 
 extension ISFEditor {
 extension ISFEditor {
     class ViewModel<Provider>: BaseViewModel<Provider>, ObservableObject where Provider: ISFEditorProvider {
     class ViewModel<Provider>: BaseViewModel<Provider>, ObservableObject where Provider: ISFEditorProvider {
-        override func subscribe() {}
+        @Injected() var settingsManager: SettingsManager!
+        @Published var items: [Item] = []
+
+        let timeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
+
+        var rateValues: [Double] {
+            switch units {
+            case .mgdL:
+                return stride(from: 9, to: 540.01, by: 1.0).map { $0 }
+            case .mmolL:
+                return stride(from: 0.5, to: 30.01, by: 0.1).map { $0 }
+            }
+        }
+
+        var canAdd: Bool {
+            guard let lastItem = items.last else { return true }
+            return lastItem.timeIndex < timeValues.count - 1
+        }
+
+        private(set) var units: GlucoseUnits = .mmolL
+
+        override func subscribe() {
+            units = settingsManager.settings.units
+            let profile = provider.profile
+            items = profile.sensitivities.map { value in
+                let timeIndex = timeValues.firstIndex(of: Double(value.offset * 60)) ?? 0
+                let rateIndex = rateValues.firstIndex(of: Double(value.sensitivity)) ?? 0
+                return Item(rateIndex: rateIndex, selectedIndex: timeIndex)
+            }
+        }
+
+        func add() {
+            var selected = 0
+            var rate = 0
+            if let last = items.last {
+                selected = last.timeIndex + 1
+                rate = last.rateIndex
+            }
+
+            let newItem = Item(rateIndex: rate, selectedIndex: selected)
+
+            items.append(newItem)
+        }
+
+        func save() {
+            let sensitivities = items.enumerated().map { _, item -> InsulinSensitivityEntry in
+                let fotmatter = DateFormatter()
+                fotmatter.timeZone = TimeZone(secondsFromGMT: 0)
+                fotmatter.dateFormat = "HH:mm:ss"
+                let date = Date(timeIntervalSince1970: self.timeValues[item.timeIndex])
+                let minutes = Int(date.timeIntervalSince1970 / 60)
+                let rate = Decimal(self.rateValues[item.rateIndex])
+                return InsulinSensitivityEntry(sensitivity: rate, offset: minutes, start: fotmatter.string(from: date))
+            }
+            let profile = InsulinSensitivities(units: units, userPrefferedUnits: units, sensitivities: sensitivities)
+            provider.saveProfile(profile)
+        }
+
+        func validate() {
+            DispatchQueue.main.async {
+                let uniq = Array(Set(self.items))
+                let sorted = uniq.sorted { $0.timeIndex < $1.timeIndex }
+                sorted.first?.timeIndex = 0
+                self.items = sorted
+            }
+        }
     }
     }
 }
 }

+ 120 - 4
FreeAPS/Sources/Modules/ISFEditor/View/ISFEditorRootView.swift

@@ -3,12 +3,128 @@ import SwiftUI
 extension ISFEditor {
 extension ISFEditor {
     struct RootView: BaseView {
     struct RootView: BaseView {
         @EnvironmentObject var viewModel: ViewModel<Provider>
         @EnvironmentObject var viewModel: ViewModel<Provider>
+        @State private var editMode = EditMode.inactive
+
+        private var dateFormatter: DateFormatter {
+            let formatter = DateFormatter()
+            formatter.timeZone = TimeZone(secondsFromGMT: 0)
+            formatter.timeStyle = .short
+            return formatter
+        }
+
+        private var rateFormatter: NumberFormatter {
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            return formatter
+        }
 
 
         var body: some View {
         var body: some View {
-            Text("ISFEditor screen")
-                .navigationTitle("ISFEditor")
-                .navigationBarTitleDisplayMode(.automatic)
-                .navigationBarItems(leading: Button("Close", action: viewModel.hideModal))
+            Form {
+                Section(header: Text("Schedule")) {
+                    list
+                    addButton
+                }
+                Section {
+                    Button { viewModel.save() }
+                    label: {
+                        Text("Save")
+                    }
+                    .disabled(viewModel.items.isEmpty)
+                }
+            }
+            .navigationTitle("Insulin Sensitivities")
+            .navigationBarTitleDisplayMode(.automatic)
+            .navigationBarItems(
+                leading: Button("Close", action: viewModel.hideModal),
+                trailing: EditButton()
+            )
+            .environment(\.editMode, $editMode)
+            .onAppear {
+                viewModel.validate()
+            }
+        }
+
+        private func pickers(for index: Int) -> some View {
+            GeometryReader { geometry in
+                VStack {
+                    HStack {
+                        Text("Rate").frame(width: geometry.size.width / 2)
+                        Text("Time").frame(width: geometry.size.width / 2)
+                    }
+                    HStack(spacing: 0) {
+                        Picker(selection: $viewModel.items[index].rateIndex, label: EmptyView()) {
+                            ForEach(0 ..< viewModel.rateValues.count, id: \.self) { i in
+                                Text(
+                                    (
+                                        self.rateFormatter
+                                            .string(from: viewModel.rateValues[i] as NSNumber) ?? ""
+                                    ) + " \(viewModel.units.rawValue)/U"
+                                ).tag(i)
+                            }
+                        }
+                        .frame(maxWidth: geometry.size.width / 2)
+                        .clipped()
+
+                        Picker(selection: $viewModel.items[index].timeIndex, label: EmptyView()) {
+                            ForEach(0 ..< viewModel.timeValues.count, id: \.self) { i in
+                                Text(
+                                    self.dateFormatter
+                                        .string(from: Date(
+                                            timeIntervalSince1970: viewModel
+                                                .timeValues[i]
+                                        ))
+                                ).tag(i)
+                            }
+                        }
+                        .frame(maxWidth: geometry.size.width / 2)
+                        .clipped()
+                    }
+                }
+            }
+        }
+
+        private var list: some View {
+            List {
+                ForEach(viewModel.items.indexed(), id: \.1.id) { index, item in
+                    NavigationLink(destination: pickers(for: index)) {
+                        HStack {
+                            Text("Rate").foregroundColor(.secondary)
+                            Text(
+                                "\(rateFormatter.string(from: viewModel.rateValues[item.rateIndex] as NSNumber) ?? "0") \(viewModel.units.rawValue)/U"
+                            )
+                            Spacer()
+                            Text("starts at").foregroundColor(.secondary)
+                            Text(
+                                "\(dateFormatter.string(from: Date(timeIntervalSince1970: viewModel.timeValues[item.timeIndex])))"
+                            )
+                        }
+                    }
+                    .moveDisabled(true)
+                }
+                .onDelete(perform: onDelete)
+            }
+        }
+
+        private var addButton: some View {
+            guard viewModel.canAdd else {
+                return AnyView(EmptyView())
+            }
+
+            switch editMode {
+            case .inactive:
+                return AnyView(Button(action: onAdd) { Text("Add") })
+            default:
+                return AnyView(EmptyView())
+            }
+        }
+
+        func onAdd() {
+            viewModel.add()
+        }
+
+        private func onDelete(offsets: IndexSet) {
+            viewModel.items.remove(atOffsets: offsets)
+            viewModel.validate()
         }
         }
     }
     }
 }
 }

+ 1 - 1
FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift

@@ -40,7 +40,7 @@ extension Settings {
                         Text("BG targets").chevronCell().modal(for: .configEditor(file: OpenAPS.Settings.bgTargets), from: self)
                         Text("BG targets").chevronCell().modal(for: .configEditor(file: OpenAPS.Settings.bgTargets), from: self)
                         Text("Carb ratios").chevronCell().modal(for: .configEditor(file: OpenAPS.Settings.carbRatios), from: self)
                         Text("Carb ratios").chevronCell().modal(for: .configEditor(file: OpenAPS.Settings.carbRatios), from: self)
                         Text("Insulin sensitivities").chevronCell()
                         Text("Insulin sensitivities").chevronCell()
-                            .modal(for: .configEditor(file: OpenAPS.Settings.carbRatios), from: self)
+                            .modal(for: .configEditor(file: OpenAPS.Settings.insulinSensitivities), from: self)
                         Text("Temp targets").chevronCell()
                         Text("Temp targets").chevronCell()
                             .modal(for: .configEditor(file: OpenAPS.Settings.tempTargets), from: self)
                             .modal(for: .configEditor(file: OpenAPS.Settings.tempTargets), from: self)
                     }
                     }