|
|
@@ -13,12 +13,34 @@ protocol APSManager {
|
|
|
var pumpDisplayState: CurrentValueSubject<PumpDisplayState?, Never> { get }
|
|
|
var pumpName: CurrentValueSubject<String, Never> { get }
|
|
|
var isLooping: CurrentValueSubject<Bool, Never> { get }
|
|
|
- var lastLoopDate: PassthroughSubject<Date, Never> { get }
|
|
|
+ var lastLoopDate: Date { get }
|
|
|
+ var lastLoopDateSubject: PassthroughSubject<Date, Never> { get }
|
|
|
var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> { get }
|
|
|
func enactTempBasal(rate: Double, duration: TimeInterval)
|
|
|
func makeProfiles() -> AnyPublisher<Bool, Never>
|
|
|
func determineBasal() -> AnyPublisher<Bool, Never>
|
|
|
func roundBolus(amount: Decimal) -> Decimal
|
|
|
+ var lastError: CurrentValueSubject<Error?, Never> { get }
|
|
|
+}
|
|
|
+
|
|
|
+enum APSError: LocalizedError {
|
|
|
+ case pumpError(Error)
|
|
|
+ case invalidPumpState(message: String)
|
|
|
+ case glucoseError(message: String)
|
|
|
+ case apsError(message: String)
|
|
|
+
|
|
|
+ var errorDescription: String? {
|
|
|
+ switch self {
|
|
|
+ case let .pumpError(error):
|
|
|
+ return "Pump error: \(error.localizedDescription)"
|
|
|
+ case let .invalidPumpState(message):
|
|
|
+ return "Invalid Pump State: \(message)"
|
|
|
+ case let .glucoseError(message):
|
|
|
+ return "Invalid glucose: \(message)"
|
|
|
+ case let .apsError(message):
|
|
|
+ return "APS error: \(message)"
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
final class BaseAPSManager: APSManager, Injectable {
|
|
|
@@ -34,6 +56,11 @@ final class BaseAPSManager: APSManager, Injectable {
|
|
|
@Injected() private var settingsManager: SettingsManager!
|
|
|
@Injected() private var broadcaster: Broadcaster!
|
|
|
@Persisted(key: "lastAutotuneDate") private var lastAutotuneDate: Date = .distantPast
|
|
|
+ @Persisted(key: "lastLoopDate") var lastLoopDate: Date = .distantPast {
|
|
|
+ didSet {
|
|
|
+ lastLoopDateSubject.send(lastLoopDate)
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
private var openAPS: OpenAPS!
|
|
|
|
|
|
@@ -45,7 +72,8 @@ final class BaseAPSManager: APSManager, Injectable {
|
|
|
}
|
|
|
|
|
|
let isLooping = CurrentValueSubject<Bool, Never>(false)
|
|
|
- let lastLoopDate = PassthroughSubject<Date, Never>()
|
|
|
+ let lastLoopDateSubject = PassthroughSubject<Date, Never>()
|
|
|
+ let lastError = CurrentValueSubject<Error?, Never>(nil)
|
|
|
|
|
|
var pumpDisplayState: CurrentValueSubject<PumpDisplayState?, Never> {
|
|
|
deviceDataManager.pumpDisplayState
|
|
|
@@ -72,11 +100,20 @@ final class BaseAPSManager: APSManager, Injectable {
|
|
|
|
|
|
private func subscribe() {
|
|
|
deviceDataManager.recommendsLoop
|
|
|
+ .receive(on: processQueue)
|
|
|
.sink { [weak self] in
|
|
|
self?.fetchAndLoop()
|
|
|
}
|
|
|
.store(in: &lifetime)
|
|
|
pumpManager?.addStatusObserver(self, queue: processQueue)
|
|
|
+
|
|
|
+ deviceDataManager.errorSubject
|
|
|
+ .receive(on: processQueue)
|
|
|
+ .map { APSError.pumpError($0) }
|
|
|
+ .sink {
|
|
|
+ self.processError($0)
|
|
|
+ }
|
|
|
+ .store(in: &lifetime)
|
|
|
}
|
|
|
|
|
|
func heartbeat(date: Date, force: Bool) {
|
|
|
@@ -116,7 +153,7 @@ final class BaseAPSManager: APSManager, Injectable {
|
|
|
self.enactSuggested()
|
|
|
} else {
|
|
|
self.isLooping.send(false)
|
|
|
- self.lastLoopDate.send(Date())
|
|
|
+ self.lastLoopDate = Date()
|
|
|
}
|
|
|
} else {
|
|
|
self.isLooping.send(false)
|
|
|
@@ -127,18 +164,21 @@ final class BaseAPSManager: APSManager, Injectable {
|
|
|
private func verifyStatus() -> Bool {
|
|
|
guard let pump = pumpManager else {
|
|
|
debug(.apsManager, "Pump is not set")
|
|
|
+ processError(APSError.invalidPumpState(message: "Pump is not set"))
|
|
|
return false
|
|
|
}
|
|
|
let status = pump.status.pumpStatus
|
|
|
|
|
|
guard !status.bolusing, !status.suspended else {
|
|
|
debug(.apsManager, "Pump is bolusing or suspended")
|
|
|
+ processError(APSError.invalidPumpState(message: "Pump is bolusing or suspended"))
|
|
|
return false
|
|
|
}
|
|
|
|
|
|
let reservoir = storage.retrieve(OpenAPS.Monitor.reservoir, as: Decimal.self) ?? 100
|
|
|
guard reservoir > 0 else {
|
|
|
debug(.apsManager, "Reservoir is empty")
|
|
|
+ processError(APSError.invalidPumpState(message: "Reservoir is empty"))
|
|
|
return false
|
|
|
}
|
|
|
|
|
|
@@ -160,12 +200,14 @@ final class BaseAPSManager: APSManager, Injectable {
|
|
|
func determineBasal() -> AnyPublisher<Bool, Never> {
|
|
|
guard let glucose = storage.retrieve(OpenAPS.Monitor.glucose, as: [BloodGlucose].self), glucose.count >= 36 else {
|
|
|
debug(.apsManager, "Not enough glucose data")
|
|
|
+ processError(APSError.glucoseError(message: "Not enough glucose data"))
|
|
|
return Just(false).eraseToAnyPublisher()
|
|
|
}
|
|
|
|
|
|
let lastGlucoseDate = glucoseStorage.lastGlucoseDate()
|
|
|
guard lastGlucoseDate >= Date().addingTimeInterval(-12.minutes.timeInterval) else {
|
|
|
debug(.apsManager, "Glucose data is stale")
|
|
|
+ processError(APSError.glucoseError(message: "Glucose data is stale"))
|
|
|
return Just(false).eraseToAnyPublisher()
|
|
|
}
|
|
|
|
|
|
@@ -237,6 +279,7 @@ final class BaseAPSManager: APSManager, Injectable {
|
|
|
}
|
|
|
case let .failure(error):
|
|
|
debug(.apsManager, "Bolus failed with error: \(error.localizedDescription)")
|
|
|
+ self.processError(APSError.pumpError(error))
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -253,6 +296,7 @@ final class BaseAPSManager: APSManager, Injectable {
|
|
|
self.storage.save(temp, as: OpenAPS.Monitor.tempBasal)
|
|
|
case let .failure(error):
|
|
|
debug(.apsManager, "Temp Basal failed with error: \(error.localizedDescription)")
|
|
|
+ self.processError(APSError.pumpError(error))
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -366,18 +410,21 @@ final class BaseAPSManager: APSManager, Injectable {
|
|
|
guard let suggested = storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self) else {
|
|
|
isLooping.send(false)
|
|
|
debug(.apsManager, "Suggestion not found")
|
|
|
+ processError(APSError.apsError(message: "Suggestion not found"))
|
|
|
return
|
|
|
}
|
|
|
|
|
|
guard Date().timeIntervalSince(suggested.deliverAt ?? .distantPast) < Config.eхpirationInterval else {
|
|
|
isLooping.send(false)
|
|
|
debug(.apsManager, "Suggestion expired")
|
|
|
+ processError(APSError.apsError(message: "Suggestion expired"))
|
|
|
return
|
|
|
}
|
|
|
|
|
|
guard let pump = pumpManager, verifyStatus() else {
|
|
|
isLooping.send(false)
|
|
|
- debug(.apsManager, "Invalid pump status")
|
|
|
+ debug(.apsManager, "Invalid pump state")
|
|
|
+ processError(APSError.invalidPumpState(message: "Pump is bolusing or suspended"))
|
|
|
return
|
|
|
}
|
|
|
|
|
|
@@ -409,13 +456,15 @@ final class BaseAPSManager: APSManager, Injectable {
|
|
|
if case let .failure(error) = completion {
|
|
|
debug(.apsManager, "Loop failed with error: \(error.localizedDescription)")
|
|
|
self?.reportEnacted(suggestion: suggested, received: false)
|
|
|
+ self?.processError(APSError.pumpError(error))
|
|
|
} else {
|
|
|
self?.reportEnacted(suggestion: suggested, received: true)
|
|
|
}
|
|
|
self?.isLooping.send(false)
|
|
|
} receiveValue: {
|
|
|
debug(.apsManager, "Loop succeeded")
|
|
|
- self.lastLoopDate.send(Date())
|
|
|
+ self.lastError.send(nil)
|
|
|
+ self.lastLoopDate = Date()
|
|
|
}.store(in: &lifetime)
|
|
|
}
|
|
|
|
|
|
@@ -434,6 +483,11 @@ final class BaseAPSManager: APSManager, Injectable {
|
|
|
nightscout.uploadStatus()
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ private func processError(_ error: Error) {
|
|
|
+ debug(.apsManager, "\(error.localizedDescription)")
|
|
|
+ lastError.send(error)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
private extension PumpManager {
|