Przeglądaj źródła

Merge remote-tracking branch 'refs/remotes/origin/Crowdin'

Jon Mårtensson 4 lat temu
rodzic
commit
3cc10207c6

+ 2 - 0
.gitignore

@@ -77,3 +77,5 @@ fastlane/Preview.html
 fastlane/screenshots
 fastlane/test_output
 fastlane/FastlaneRunner
+
+ConfigOverride.xcconfig

+ 7 - 0
Config.xcconfig

@@ -0,0 +1,7 @@
+APP_DISPLAY_NAME = FreeAPS X
+BUILD_VERSION = 0.2.3
+DEVELOPER_TEAM = ##TEAM_ID##
+BUNDLE_IDENTIFIER = ru.artpancreas.$(DEVELOPMENT_TEAM).FreeAPS
+APP_GROUP_ID = group.com.$(DEVELOPMENT_TEAM).loopkit.LoopGroups
+
+#include? "ConfigOverride.xcconfig"

+ 12 - 2
Dependencies/rileylink_ios/OmniKit/PumpManager/OmnipodPumpManager.swift

@@ -1425,18 +1425,28 @@ extension OmnipodPumpManager: PumpManager {
                 state.bolusEngageState = .engaging
             })
 
-            // If pod suspended, resume basal before bolusing to match existing Medtronic PumpManager behavior
+            // Initialize to true to match existing Medtronic PumpManager behavior for any
+            // manual boluses or to false to never auto resume a suspended pod for any bolus.
+            let autoResumeOnManualBolus = true
+
             if case .some(.suspended) = self.state.podState?.suspendState {
+                // Pod suspended, only auto resume for a manual bolus if autoResumeOnManualBolus is true
+                if automatic || autoResumeOnManualBolus == false {
+                    self.log.error("enactBolus: returning pod suspended error for %@ bolus", automatic ? "automatic" : "manual")
+                    completion(.failure(PumpManagerError.deviceState(PodCommsError.podSuspended)))
+                    return
+                }
                 do {
                     let scheduleOffset = self.state.timeZone.scheduleOffset(forDate: Date())
                     let beep = self.confirmationBeeps
                     let podStatus = try session.resumeBasal(schedule: self.state.basalSchedule, scheduleOffset: scheduleOffset, acknowledgementBeep: beep, completionBeep: beep)
+                    try session.cancelSuspendAlerts()
                     guard podStatus.deliveryStatus.bolusing == false else {
                         completion(.failure(PumpManagerError.deviceState(PodCommsError.unfinalizedBolus)))
                         return
                     }
                 } catch let error {
-                    self.log.error("enactBolus: error resuming suspended pod: %s", String(describing: error))
+                    self.log.error("enactBolus: error resuming suspended pod: %@", String(describing: error))
                     completion(.failure(PumpManagerError.communication(error as? LocalizedError)))
                     return
                 }

+ 1 - 1
Dependencies/rileylink_ios/OmniKit/PumpManager/PodCommsSession.swift

@@ -123,7 +123,7 @@ extension PodCommsError: LocalizedError {
         case .nonceResyncFailed:
             return nil
         case .podSuspended:
-            return nil
+            return LocalizedString("Resume delivery", comment: "Recovery suggestion when pod is suspended")
         case .podFault:
             return nil
         case .commsError:

+ 15 - 9
FreeAPS.xcodeproj/project.pbxproj

@@ -243,6 +243,7 @@
 		E00EEC0627368630002FF094 /* UIAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = E00EEC0027368630002FF094 /* UIAssembly.swift */; };
 		E00EEC0727368630002FF094 /* APSAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = E00EEC0127368630002FF094 /* APSAssembly.swift */; };
 		E00EEC0827368630002FF094 /* NetworkAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = E00EEC0227368630002FF094 /* NetworkAssembly.swift */; };
+		E013D872273AC6FE0014109C /* GlucoseSimulatorSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E013D871273AC6FE0014109C /* GlucoseSimulatorSource.swift */; };
 		E13B7DAB2A435F57066AF02E /* TargetsEditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36F58DDD71F0E795464FA3F0 /* TargetsEditorStateModel.swift */; };
 		E25073BC86C11C3D6A42F5AC /* CalibrationsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47DFCE895C930F784EF11843 /* CalibrationsStateModel.swift */; };
 		E39E418C56A5A46B61D960EE /* ConfigEditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D5B4F8B4194BB7E260EF251 /* ConfigEditorStateModel.swift */; };
@@ -378,6 +379,7 @@
 		3811DEE725CA063400A708ED /* PersistedProperty.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PersistedProperty.swift; sourceTree = "<group>"; };
 		3811DF0125CA9FEA00A708ED /* Credentials.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Credentials.swift; sourceTree = "<group>"; };
 		3811DF0F25CAAAE200A708ED /* APSManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APSManager.swift; sourceTree = "<group>"; };
+		3818AA42274BBC1100843DB3 /* ConfigOverride.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ConfigOverride.xcconfig; sourceTree = "<group>"; };
 		38192E03261B82FA0094D973 /* ReachabilityManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReachabilityManager.swift; sourceTree = "<group>"; };
 		38192E06261BA9960094D973 /* FetchTreatmentsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchTreatmentsManager.swift; sourceTree = "<group>"; };
 		38192E0C261BAF980094D973 /* ConvenienceExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConvenienceExtensions.swift; sourceTree = "<group>"; };
@@ -560,6 +562,7 @@
 		E00EEC0027368630002FF094 /* UIAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIAssembly.swift; sourceTree = "<group>"; };
 		E00EEC0127368630002FF094 /* APSAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APSAssembly.swift; sourceTree = "<group>"; };
 		E00EEC0227368630002FF094 /* NetworkAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkAssembly.swift; sourceTree = "<group>"; };
+		E013D871273AC6FE0014109C /* GlucoseSimulatorSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseSimulatorSource.swift; sourceTree = "<group>"; };
 		E2EBA7C03C26FCC67E16D798 /* LibreConfigProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LibreConfigProvider.swift; sourceTree = "<group>"; };
 		E68CDC1E5C438D1BEAD4CF24 /* LibreConfigStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LibreConfigStateModel.swift; sourceTree = "<group>"; };
 		E9AAB83FB6C3B41EFD1846A0 /* AddTempTargetRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddTempTargetRootView.swift; sourceTree = "<group>"; };
@@ -871,7 +874,6 @@
 				388E596425AD948E0019842D /* Info.plist */,
 				1927C8E82744606D00347C69 /* InfoPlist.strings */,
 				388E595F25AD948E0019842D /* Assets.xcassets */,
-				38F3783A2613555C009DB701 /* Config.xcconfig */,
 			);
 			path = Resources;
 			sourceTree = "<group>";
@@ -935,6 +937,7 @@
 				38569344270B5DFA0002C50D /* CGMType.swift */,
 				386A124E271707F000DDC61C /* DexcomSource.swift */,
 				38569345270B5DFA0002C50D /* GlucoseSource.swift */,
+				E013D871273AC6FE0014109C /* GlucoseSimulatorSource.swift */,
 				38FEF407273B011A00574A46 /* LibreTransmitterSource.swift */,
 				3862CC03273D150600BF832C /* Calibrations */,
 			);
@@ -972,6 +975,8 @@
 		388E594F25AD948C0019842D = {
 			isa = PBXGroup;
 			children = (
+				3818AA42274BBC1100843DB3 /* ConfigOverride.xcconfig */,
+				38F3783A2613555C009DB701 /* Config.xcconfig */,
 				388E595A25AD948C0019842D /* FreeAPS */,
 				38FCF3EE25E9028E0078B0D1 /* FreeAPSTests */,
 				388E595925AD948C0019842D /* Products */,
@@ -1698,6 +1703,7 @@
 				3811DE4225C9D4A100A708ED /* SettingsDataFlow.swift in Sources */,
 				3811DE2525C9D48300A708ED /* MainRootView.swift in Sources */,
 				3811DE3125C9D49500A708ED /* HomeProvider.swift in Sources */,
+				E013D872273AC6FE0014109C /* GlucoseSimulatorSource.swift in Sources */,
 				388E5A5C25B6F0770019842D /* JSON.swift in Sources */,
 				3811DF0225CA9FEA00A708ED /* Credentials.swift in Sources */,
 				389A572026079BAA00BC102F /* Interpolation.swift in Sources */,
@@ -2059,14 +2065,14 @@
 		388E596825AD948E0019842D /* Debug */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
-				APP_GROUP_ID = "group.com.${DEVELOPMENT_TEAM}.loopkit.LoopGroup";
+				APP_GROUP_ID = "$(APP_GROUP_ID)";
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CODE_SIGN_ENTITLEMENTS = FreeAPS/Resources/FreeAPS.entitlements;
 				CODE_SIGN_STYLE = Automatic;
 				CURRENT_PROJECT_VERSION = 1;
 				DEVELOPMENT_ASSET_PATHS = "";
-				DEVELOPMENT_TEAM = BA7ZHP4963;
+				DEVELOPMENT_TEAM = "${DEVELOPER_TEAM}";
 				ENABLE_PREVIEWS = YES;
 				INFOPLIST_FILE = FreeAPS/Resources/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
@@ -2075,7 +2081,7 @@
 					"@executable_path/Frameworks",
 				);
 				MARKETING_VERSION = "$(CURRENT_PROJECT_VERSION)";
-				PRODUCT_BUNDLE_IDENTIFIER = ru.artpancreas.FreeAPS;
+				PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER)";
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SWIFT_VERSION = 5.0;
 				TARGETED_DEVICE_FAMILY = "1,2";
@@ -2085,14 +2091,14 @@
 		388E596925AD948E0019842D /* Release */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
-				APP_GROUP_ID = "group.com.${DEVELOPMENT_TEAM}.loopkit.LoopGroup";
+				APP_GROUP_ID = "$(APP_GROUP_ID)";
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CODE_SIGN_ENTITLEMENTS = FreeAPS/Resources/FreeAPS.entitlements;
 				CODE_SIGN_STYLE = Automatic;
 				CURRENT_PROJECT_VERSION = 1;
 				DEVELOPMENT_ASSET_PATHS = "";
-				DEVELOPMENT_TEAM = BA7ZHP4963;
+				DEVELOPMENT_TEAM = "${DEVELOPER_TEAM}";
 				ENABLE_PREVIEWS = YES;
 				INFOPLIST_FILE = FreeAPS/Resources/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
@@ -2101,7 +2107,7 @@
 					"@executable_path/Frameworks",
 				);
 				MARKETING_VERSION = "$(CURRENT_PROJECT_VERSION)";
-				PRODUCT_BUNDLE_IDENTIFIER = ru.artpancreas.FreeAPS;
+				PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER)";
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SWIFT_VERSION = 5.0;
 				TARGETED_DEVICE_FAMILY = "1,2";
@@ -2113,7 +2119,7 @@
 			buildSettings = {
 				BUNDLE_LOADER = "$(TEST_HOST)";
 				CODE_SIGN_STYLE = Automatic;
-				DEVELOPMENT_TEAM = 777258T3K8;
+				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
 				INFOPLIST_FILE = FreeAPSTests/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 14.4;
 				LD_RUNPATH_SEARCH_PATHS = (
@@ -2134,7 +2140,7 @@
 			buildSettings = {
 				BUNDLE_LOADER = "$(TEST_HOST)";
 				CODE_SIGN_STYLE = Automatic;
-				DEVELOPMENT_TEAM = 777258T3K8;
+				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
 				INFOPLIST_FILE = FreeAPSTests/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 14.4;
 				LD_RUNPATH_SEARCH_PATHS = (

+ 2 - 2
FreeAPS.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -24,8 +24,8 @@
         "repositoryURL": "https://github.com/ivalkou/LibreTransmitterX",
         "state": {
           "branch": null,
-          "revision": "af874b58f03554d92053e06d33fd1b638b721552",
-          "version": "1.0.8"
+          "revision": "9c5d68262eef51365114213ebd4dee91687eeeed",
+          "version": "1.0.12"
         }
       },
       {

+ 0 - 1
FreeAPS/Resources/Config.xcconfig

@@ -1 +0,0 @@
-BUILD_VERSION = 0.2.3

+ 1 - 1
FreeAPS/Resources/FreeAPS.entitlements

@@ -9,7 +9,7 @@
 	</array>
 	<key>com.apple.security.application-groups</key>
 	<array>
-		<string>group.com.${DEVELOPMENT_TEAM}.loopkit.LoopGroup</string>
+		<string>$(APP_GROUP_ID)</string>
 	</array>
 </dict>
 </plist>

+ 3 - 3
FreeAPS/Resources/Info.plist

@@ -13,7 +13,7 @@
 	<key>CFBundleInfoDictionaryVersion</key>
 	<string>6.0</string>
 	<key>CFBundleName</key>
-	<string>FreeAPS X</string>
+	<string>$(APP_DISPLAY_NAME)</string>
 	<key>CFBundlePackageType</key>
 	<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
 	<key>CFBundleShortVersionString</key>
@@ -56,6 +56,8 @@
 	<string>Bluetooth is used to communicate with insulin pump and continuous glucose monitor devices</string>
 	<key>NSBluetoothPeripheralUsageDescription</key>
 	<string>Bluetooth is used to communicate with insulin pump and continuous glucose monitor devices</string>
+	<key>NSCalendarsUsageDescription</key>
+	<string>Calendar is used to create a new glucose events.</string>
 	<key>NSFaceIDUsageDescription</key>
 	<string>For authorized acces to bolus</string>
 	<key>UIApplicationSceneManifest</key>
@@ -65,8 +67,6 @@
 	</dict>
 	<key>UIApplicationSupportsIndirectInputEvents</key>
 	<true/>
-	<key>NSCalendarsUsageDescription</key>
-	<string>Calendar is used to create a new glucose events.</string>
 	<key>UIBackgroundModes</key>
 	<array>
 		<string>bluetooth-central</string>

+ 1 - 1
FreeAPS/Sources/APS/APSManager.swift

@@ -535,7 +535,7 @@ final class BaseAPSManager: APSManager, Injectable {
     }
 
     private func reportEnacted(suggestion: Suggestion, received: Bool) {
-        if suggestion.deliverAt != nil, suggestion.rate != nil || suggestion.units != nil {
+        if suggestion.deliverAt != nil {
             var enacted = suggestion
             enacted.timestamp = Date()
             enacted.recieved = received

+ 5 - 0
FreeAPS/Sources/APS/CGM/CGMType.swift

@@ -7,6 +7,7 @@ enum CGMType: String, JSON, CaseIterable, Identifiable {
     case xdrip
     case dexcomG6
     case dexcomG5
+    case simulator
     case libreTransmitter
 
     var displayName: String {
@@ -19,6 +20,8 @@ enum CGMType: String, JSON, CaseIterable, Identifiable {
             return "Dexcom G6"
         case .dexcomG5:
             return "Dexcom G5"
+        case .simulator:
+            return NSLocalizedString("Glucose Simulator", comment: "Glucose Simulator CGM type")
         case .libreTransmitter:
             return NSLocalizedString("Libre Transmitter", comment: "Libre Transmitter type")
         }
@@ -34,6 +37,8 @@ enum CGMType: String, JSON, CaseIterable, Identifiable {
             return URL(string: "dexcomg6://")!
         case .dexcomG5:
             return URL(string: "dexcomgcgm://")!
+        case .simulator:
+            return nil
         case .libreTransmitter:
             return URL(string: "freeaps-x://libre-transmitter")!
         }

+ 180 - 0
FreeAPS/Sources/APS/CGM/GlucoseSimulatorSource.swift

@@ -0,0 +1,180 @@
+/// Glucose source - Blood Glucose Simulator
+///
+/// Source publish fake data about glucose's level, creates ascending and descending trends
+///
+/// Enter point of Source is GlucoseSimulatorSource.fetch method. Method is called from FetchGlucoseManager module.
+/// Not more often than a specified period (default - 300 seconds), it returns a Combine-publisher that publishes data on glucose values (global type BloodGlucose). If there is no up-to-date data (or the publication period has not passed yet), then a publisher of type Empty is returned, otherwise it returns a publisher of type Just.
+///
+/// Simulator composition
+/// ===================
+///
+/// class GlucoseSimulatorSource - main class
+/// protocol BloodGlucoseGenerator
+///  - IntelligentGenerator: BloodGlucoseGenerator
+
+// TODO: Every itteration trend make two steps, but must only one
+
+// TODO: Trend's value sticks to max and min Glucose value (in Glucose Generator)
+
+// TODO: Add reaction to insulin
+
+// TODO: Add probability to set trend's target value. Middle values must have more probability, than max and min.
+
+import Combine
+import Foundation
+
+// MARK: - Glucose simulator
+
+final class GlucoseSimulatorSource: GlucoseSource {
+    private enum Config {
+        // min time period to publish data
+        static let workInterval: TimeInterval = 300
+        // default BloodGlucose item at first run
+        // 288 = 1 day * 24 hours * 60 minites * 60 seconds / workInterval
+        static let defaultBGItems = 288
+    }
+
+    @Persisted(key: "GlucoseSimulatorLastGlucose") private var lastGlucose = 100
+
+    @Persisted(key: "GlucoseSimulatorLastFetchDate") private var lastFetchDate: Date! = nil
+
+    init() {
+        if lastFetchDate == nil {
+            var lastDate = Date()
+            for _ in 1 ... Config.defaultBGItems {
+                lastDate = lastDate.addingTimeInterval(-Config.workInterval)
+            }
+            lastFetchDate = lastDate
+        }
+    }
+
+    private lazy var generator: BloodGlucoseGenerator = {
+        IntelligentGenerator(
+            currentGlucose: lastGlucose
+        )
+    }()
+
+    private var canGenerateNewValues: Bool {
+        guard let lastDate = lastFetchDate else { return true }
+        if Calendar.current.dateComponents([.second], from: lastDate, to: Date()).second! >= Int(Config.workInterval) {
+            return true
+        } else {
+            return false
+        }
+    }
+
+    func fetch() -> AnyPublisher<[BloodGlucose], Never> {
+        guard canGenerateNewValues else {
+            return Empty().eraseToAnyPublisher()
+        }
+
+        let glucoses = generator.getBloodGlucoses(
+            startDate: lastFetchDate,
+            finishDate: Date(),
+            withInterval: Config.workInterval
+        )
+
+        if let lastItem = glucoses.last {
+            lastGlucose = lastItem.glucose!
+            lastFetchDate = Date()
+        }
+
+        return Just(glucoses.reversed()).eraseToAnyPublisher()
+    }
+}
+
+// MARK: - Glucose generator
+
+protocol BloodGlucoseGenerator {
+    func getBloodGlucoses(startDate: Date, finishDate: Date, withInterval: TimeInterval) -> [BloodGlucose]
+}
+
+class IntelligentGenerator: BloodGlucoseGenerator {
+    private enum Config {
+        // max and min glucose of trend's target
+        static let maxGlucose = 320
+        static let minGlucose = 45
+    }
+
+    // target glucose of trend
+    @Persisted(key: "GlucoseSimulatorTargetValue") private var trendTargetValue = 100
+    // how many steps left in current trend
+    @Persisted(key: "GlucoseSimulatorTargetSteps") private var trendStepsLeft = 1
+    // direction of last step
+    @Persisted(key: "GlucoseSimulatorDirection") private var trandsStepDirection = BloodGlucose.Direction.flat.rawValue
+    var currentGlucose: Int
+
+    init(currentGlucose: Int) {
+        self.currentGlucose = currentGlucose
+    }
+
+    func getBloodGlucoses(startDate: Date, finishDate: Date, withInterval interval: TimeInterval) -> [BloodGlucose] {
+        var result = [BloodGlucose]()
+
+        var _currentDate = startDate
+        while _currentDate <= finishDate {
+            result.append(getNextBloodGlucose(forDate: _currentDate))
+            _currentDate = _currentDate.addingTimeInterval(interval)
+        }
+
+        return result
+    }
+
+    // get next glucose's value in current trend
+    private func getNextBloodGlucose(forDate date: Date) -> BloodGlucose {
+        let previousGlucose = currentGlucose
+        makeStepInTrend()
+        trandsStepDirection = getDirection(fromGlucose: previousGlucose, toGlucose: currentGlucose).rawValue
+        let glucose = BloodGlucose(
+            _id: UUID().uuidString,
+            sgv: nil,
+            direction: BloodGlucose.Direction(rawValue: trandsStepDirection),
+            date: Decimal(Int(date.timeIntervalSince1970) * 1000),
+            dateString: date,
+            unfiltered: nil,
+            filtered: nil,
+            noise: nil,
+            glucose: currentGlucose,
+            type: nil
+        )
+        return glucose
+    }
+
+    private func setNewRandomTarget() {
+        guard trendTargetValue > 0 else {
+            trendTargetValue = Array(80 ... 110).randomElement()!
+            return
+        }
+        let difference = (Array(-50 ... -20) + Array(20 ... 50)).randomElement()!
+        let _value = trendTargetValue + difference
+        if _value <= Config.minGlucose {
+            trendTargetValue = Config.minGlucose
+        } else if _value >= Config.maxGlucose {
+            trendTargetValue = Config.maxGlucose
+        } else {
+            trendTargetValue = _value
+        }
+    }
+
+    private func setNewRandomSteps() {
+        trendStepsLeft = Array(3 ... 8).randomElement()!
+    }
+
+    private func getDirection(fromGlucose from: Int, toGlucose to: Int) -> BloodGlucose.Direction {
+        BloodGlucose.Direction(trend: to - from)
+    }
+
+    private func generateNewTrend() {
+        setNewRandomTarget()
+        setNewRandomSteps()
+    }
+
+    private func makeStepInTrend() {
+        currentGlucose +=
+            Int(Double((trendTargetValue - currentGlucose) / trendStepsLeft) * [0.3, 0.6, 1, 1.3, 1.6].randomElement()!)
+        trendStepsLeft -= 1
+        if trendStepsLeft == 0 {
+            generateNewTrend()
+        }
+    }
+}

+ 3 - 0
FreeAPS/Sources/APS/FetchGlucoseManager.swift

@@ -18,6 +18,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
 
     private lazy var appGroupSource = AppGroupSource()
     private lazy var dexcomSource = DexcomSource()
+    private lazy var simulatorSource = GlucoseSimulatorSource()
 
     init(resolver: Resolver) {
         injectServices(resolver)
@@ -36,6 +37,8 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
             glucoseSource = dexcomSource
         case .nightscout:
             glucoseSource = nightscoutManager
+        case .simulator:
+            glucoseSource = simulatorSource
         case .libreTransmitter:
             glucoseSource = libreTransmitter
         }

+ 0 - 4
FreeAPS/Sources/Models/Suggestion.swift

@@ -19,10 +19,6 @@ struct Suggestion: JSON, Equatable {
     let isf: Int?
     var timestamp: Date?
     var recieved: Bool?
-
-    var isNoTempRequired: Bool {
-        reason.contains("no temp required")
-    }
 }
 
 struct Predictions: JSON, Equatable {

+ 1 - 1
FreeAPS/Sources/Modules/Home/HomeStateModel.swift

@@ -239,7 +239,7 @@ extension Home {
             if closedLoop,
                let enactedSuggestion = enactedSuggestion,
                let timestamp = enactedSuggestion.timestamp,
-               enactedSuggestion.deliverAt == suggestion.deliverAt, suggestion.rate != nil || suggestion.units != nil
+               enactedSuggestion.deliverAt == suggestion.deliverAt, enactedSuggestion.recieved == true
             {
                 statusTitle = "Enacted at \(dateFormatter.string(from: timestamp))"
             } else if let suggestedDate = suggestion.deliverAt {

+ 1 - 0
FreeAPS/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift

@@ -13,6 +13,7 @@ struct CurrentGlucoseView: View {
             formatter.minimumFractionDigits = 1
             formatter.maximumFractionDigits = 1
         }
+        formatter.roundingMode = .halfUp
         return formatter
     }
 

+ 1 - 1
FreeAPS/Sources/Modules/Home/View/Header/LoopView.swift

@@ -78,7 +78,7 @@ struct LoopView: View {
     }
 
     private var actualSuggestion: Suggestion? {
-        if closedLoop, suggestion?.rate != nil || suggestion?.units != nil || suggestion?.isNoTempRequired ?? false {
+        if closedLoop, enactedSuggestion?.recieved == true {
             return enactedSuggestion ?? suggestion
         } else {
             return suggestion