import Foundation import Swinject import WatchConnectivity protocol WatchManager {} final class BaseWatchManager: NSObject, WatchManager, Injectable { private let session: WCSession private var state = WatchState() private let processQueue = DispatchQueue(label: "BaseWatchManager.processQueue") @Injected() private var broadcaster: Broadcaster! @Injected() private var settingsManager: SettingsManager! @Injected() private var glucoseStorage: GlucoseStorage! @Injected() private var apsManager: APSManager! @Injected() private var storage: FileStorage! @Injected() private var carbsStorage: CarbsStorage! @Injected() private var tempTargetsStorage: TempTargetsStorage! private var lifetime = Lifetime() init(resolver: Resolver, session: WCSession = .default) { self.session = session super.init() injectServices(resolver) if WCSession.isSupported() { session.delegate = self session.activate() } broadcaster.register(GlucoseObserver.self, observer: self) broadcaster.register(SuggestionObserver.self, observer: self) broadcaster.register(SettingsObserver.self, observer: self) broadcaster.register(PumpHistoryObserver.self, observer: self) broadcaster.register(PumpSettingsObserver.self, observer: self) broadcaster.register(BasalProfileObserver.self, observer: self) broadcaster.register(TempTargetsObserver.self, observer: self) broadcaster.register(CarbsObserver.self, observer: self) broadcaster.register(EnactedSuggestionObserver.self, observer: self) broadcaster.register(PumpBatteryObserver.self, observer: self) broadcaster.register(PumpReservoirObserver.self, observer: self) configureState() } private func configureState() { processQueue.async { let glucoseValues = self.glucoseText() self.state.glucose = glucoseValues.glucose self.state.trend = glucoseValues.trend self.state.delta = glucoseValues.delta self.state.glucoseDate = self.glucoseStorage.recent().last?.dateString self.state.lastLoopDate = self.enactedSuggestion?.recieved == true ? self.enactedSuggestion?.deliverAt : self .apsManager.lastLoopDate self.state.bolusIncrement = self.settingsManager.preferences.bolusIncrement self.state.maxCOB = self.settingsManager.preferences.maxCOB self.state.maxBolus = self.settingsManager.pumpSettings.maxBolus self.state.carbsRequired = self.suggestion?.carbsReq let inslinRequired = self.suggestion?.insulinReq ?? 0 self.state.bolusRecommended = self.apsManager .roundBolus(amount: max(inslinRequired * self.settingsManager.settings.insulinReqFraction, 0)) self.state.iob = self.suggestion?.iob self.state.cob = self.suggestion?.cob self.state.tempTargets = self.tempTargetsStorage.presets() .map { target -> TempTargetWatchPreset in let untilDate = self.tempTargetsStorage.current().flatMap { currentTarget -> Date? in guard currentTarget.id == target.id else { return nil } let date = currentTarget.createdAt.addingTimeInterval(TimeInterval(currentTarget.duration * 60)) return date > Date() ? date : nil } return TempTargetWatchPreset( name: target.displayName, id: target.id, description: self.descriptionForTarget(target), until: untilDate ) } self.state.bolusAfterCarbs = !self.settingsManager.settings.skipBolusScreenAfterCarbs self.state.eventualBG = self.evetualBGStraing() self.sendState() } } private func sendState() { dispatchPrecondition(condition: .onQueue(processQueue)) guard let data = try? JSONEncoder().encode(state) else { warning(.service, "Cannot encode watch state") return } guard session.isReachable else { return } session.sendMessageData(data, replyHandler: nil) { error in warning(.service, "Cannot send message to watch", error: error) } } private func glucoseText() -> (glucose: String, trend: String, delta: String) { let glucose = glucoseStorage.recent() guard let lastGlucose = glucose.last, let glucoseValue = lastGlucose.glucose else { return ("--", "--", "--") } let delta = glucose.count >= 2 ? glucoseValue - (glucose[glucose.count - 2].glucose ?? 0) : nil let units = settingsManager.settings.units let glucoseText = glucoseFormatter .string(from: Double( units == .mmolL ? glucoseValue .asMmolL : Decimal(glucoseValue) ) as NSNumber)! let directionText = lastGlucose.direction?.symbol ?? "↔︎" let deltaText = delta .map { self.deltaFormatter .string(from: Double( units == .mmolL ? $0 .asMmolL : Decimal($0) ) as NSNumber)! } ?? "--" return (glucoseText, directionText, deltaText) } private func descriptionForTarget(_ target: TempTarget) -> String { let units = settingsManager.settings.units var low = target.targetBottom var high = target.targetTop if units == .mmolL { low = low?.asMmolL high = high?.asMmolL } let description = "\(targetFormatter.string(from: (low ?? 0) as NSNumber)!) - \(targetFormatter.string(from: (high ?? 0) as NSNumber)!)" + " for \(targetFormatter.string(from: target.duration as NSNumber)!) min" return description } private func evetualBGStraing() -> String? { guard let eventualBG = suggestion?.eventualBG else { return nil } let units = settingsManager.settings.units return "⇢ " + eventualFormatter.string( from: (units == .mmolL ? eventualBG.asMmolL : Decimal(eventualBG)) as NSNumber )! } private var glucoseFormatter: NumberFormatter { let formatter = NumberFormatter() formatter.numberStyle = .decimal formatter.maximumFractionDigits = 0 if settingsManager.settings.units == .mmolL { formatter.minimumFractionDigits = 1 formatter.maximumFractionDigits = 1 } formatter.roundingMode = .halfUp return formatter } private var eventualFormatter: NumberFormatter { let formatter = NumberFormatter() formatter.numberStyle = .decimal formatter.maximumFractionDigits = 2 return formatter } private var deltaFormatter: NumberFormatter { let formatter = NumberFormatter() formatter.numberStyle = .decimal formatter.maximumFractionDigits = 2 formatter.positivePrefix = "+" return formatter } private var targetFormatter: NumberFormatter { let formatter = NumberFormatter() formatter.numberStyle = .decimal formatter.maximumFractionDigits = 1 return formatter } private var suggestion: Suggestion? { storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self) } private var enactedSuggestion: Suggestion? { storage.retrieve(OpenAPS.Enact.enacted, as: Suggestion.self) } } extension BaseWatchManager: WCSessionDelegate { func sessionDidBecomeInactive(_: WCSession) {} func sessionDidDeactivate(_: WCSession) {} func session(_: WCSession, activationDidCompleteWith state: WCSessionActivationState, error _: Error?) { debug(.service, "WCSession is activated: \(state == .activated)") } func session(_: WCSession, didReceiveMessage message: [String: Any]) { debug(.service, "WCSession got message: \(message)") if let stateRequest = message["stateRequest"] as? Bool, stateRequest { processQueue.async { self.sendState() } } } func session(_: WCSession, didReceiveMessage message: [String: Any], replyHandler: @escaping ([String: Any]) -> Void) { debug(.service, "WCSession got message with reply handler: \(message)") if let carbs = message["carbs"] as? Double, carbs > 0 { carbsStorage.storeCarbs([ CarbsEntry(createdAt: Date(), carbs: Decimal(carbs), enteredBy: CarbsEntry.manual) ]) if settingsManager.settings.skipBolusScreenAfterCarbs { apsManager.determineBasalSync() replyHandler(["confirmation": true]) return } else { apsManager.determineBasal() .sink { _ in replyHandler(["confirmation": true]) } .store(in: &lifetime) return } } if let tempTargetID = message["tempTarget"] as? String { if var preset = tempTargetsStorage.presets().first(where: { $0.id == tempTargetID }) { preset.createdAt = Date() tempTargetsStorage.storeTempTargets([preset]) replyHandler(["confirmation": true]) return } else if tempTargetID == "cancel" { let entry = TempTarget( name: TempTarget.cancel, createdAt: Date(), targetTop: 0, targetBottom: 0, duration: 0, enteredBy: TempTarget.manual, reason: TempTarget.cancel ) tempTargetsStorage.storeTempTargets([entry]) replyHandler(["confirmation": true]) return } } if let bolus = message["bolus"] as? Double, bolus > 0 { apsManager.enactBolus(amount: bolus, isSMB: false) replyHandler(["confirmation": true]) return } replyHandler(["confirmation": false]) } func session(_: WCSession, didReceiveMessageData _: Data) {} func sessionReachabilityDidChange(_ session: WCSession) { if session.isReachable { processQueue.async { self.sendState() } } } } extension BaseWatchManager: GlucoseObserver, SuggestionObserver, SettingsObserver, PumpHistoryObserver, PumpSettingsObserver, BasalProfileObserver, TempTargetsObserver, CarbsObserver, EnactedSuggestionObserver, PumpBatteryObserver, PumpReservoirObserver { func glucoseDidUpdate(_: [BloodGlucose]) { configureState() } func suggestionDidUpdate(_: Suggestion) { configureState() } func settingsDidChange(_: FreeAPSSettings) { configureState() } func pumpHistoryDidUpdate(_: [PumpHistoryEvent]) { // TODO: } func pumpSettingsDidChange(_: PumpSettings) { configureState() } func basalProfileDidChange(_: [BasalProfileEntry]) { // TODO: } func tempTargetsDidUpdate(_: [TempTarget]) { configureState() } func carbsDidUpdate(_: [CarbsEntry]) { // TODO: } func enactedSuggestionDidUpdate(_: Suggestion) { configureState() } func pumpBatteryDidChange(_: Battery) { // TODO: } func pumpReservoirDidChange(_: Decimal) { // TODO: } }