AppleWatchManager.swift 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890
  1. import Combine
  2. import CoreData
  3. import Foundation
  4. import Swinject
  5. import WatchConnectivity
  6. /// Protocol defining the base functionality for Watch communication
  7. // TODO: Complete this
  8. protocol WatchManager {}
  9. /// Main implementation of the Watch communication manager
  10. /// Handles bidirectional communication between iPhone and Apple Watch
  11. final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchManager {
  12. private var session: WCSession?
  13. @Injected() var broadcaster: Broadcaster!
  14. @Injected() private var apsManager: APSManager!
  15. @Injected() private var settingsManager: SettingsManager!
  16. @Injected() private var fileStorage: FileStorage!
  17. @Injected() private var glucoseStorage: GlucoseStorage!
  18. @Injected() private var determinationStorage: DeterminationStorage!
  19. @Injected() private var overrideStorage: OverrideStorage!
  20. @Injected() private var tempTargetStorage: TempTargetsStorage!
  21. private var units: GlucoseUnits = .mgdL
  22. private var glucoseColorScheme: GlucoseColorScheme = .staticColor
  23. private var lowGlucose: Decimal = 70.0
  24. private var highGlucose: Decimal = 180.0
  25. private var currentGlucoseTarget: Decimal = 100.0
  26. private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
  27. private var subscriptions = Set<AnyCancellable>()
  28. typealias PumpEvent = PumpEventStored.EventType
  29. let backgroundContext = CoreDataStack.shared.newTaskContext()
  30. let viewContext = CoreDataStack.shared.persistentContainer.viewContext
  31. init(resolver: Resolver) {
  32. super.init()
  33. injectServices(resolver)
  34. setupWatchSession()
  35. units = settingsManager.settings.units
  36. glucoseColorScheme = settingsManager.settings.glucoseColorScheme
  37. lowGlucose = settingsManager.settings.low
  38. highGlucose = settingsManager.settings.high
  39. Task {
  40. currentGlucoseTarget = await getCurrentGlucoseTarget() ?? Decimal(100)
  41. }
  42. broadcaster.register(SettingsObserver.self, observer: self)
  43. broadcaster.register(PumpSettingsObserver.self, observer: self)
  44. broadcaster.register(PreferencesObserver.self, observer: self)
  45. // Observer for OrefDetermination and adjustments
  46. coreDataPublisher =
  47. changedObjectsOnManagedObjectContextDidSavePublisher()
  48. .receive(on: DispatchQueue.global(qos: .background))
  49. .share()
  50. .eraseToAnyPublisher()
  51. // Observer for glucose and manual glucose
  52. glucoseStorage.updatePublisher
  53. .receive(on: DispatchQueue.global(qos: .background))
  54. .sink { [weak self] _ in
  55. guard let self = self else { return }
  56. Task {
  57. let state = await self.setupWatchState()
  58. self.sendDataToWatch(state)
  59. }
  60. }
  61. .store(in: &subscriptions)
  62. registerHandlers()
  63. subscribeToBolusProgress()
  64. }
  65. private func registerHandlers() {
  66. coreDataPublisher?.filterByEntityName("OrefDetermination").sink { [weak self] _ in
  67. guard let self = self else { return }
  68. Task {
  69. let state = await self.setupWatchState()
  70. self.sendDataToWatch(state)
  71. }
  72. }.store(in: &subscriptions)
  73. // Due to the Batch insert this only is used for observing Deletion of Glucose entries
  74. coreDataPublisher?.filterByEntityName("GlucoseStored").sink { [weak self] _ in
  75. guard let self = self else { return }
  76. Task {
  77. let state = await self.setupWatchState()
  78. self.sendDataToWatch(state)
  79. }
  80. }.store(in: &subscriptions)
  81. coreDataPublisher?.filterByEntityName("OverrideStored").sink { [weak self] _ in
  82. guard let self = self else { return }
  83. Task {
  84. let state = await self.setupWatchState()
  85. self.sendDataToWatch(state)
  86. }
  87. }.store(in: &subscriptions)
  88. coreDataPublisher?.filterByEntityName("TempTargetStored").sink { [weak self] _ in
  89. guard let self = self else { return }
  90. Task {
  91. let state = await self.setupWatchState()
  92. self.sendDataToWatch(state)
  93. }
  94. }.store(in: &subscriptions)
  95. }
  96. /// Sets up the WatchConnectivity session if the device supports it
  97. private func setupWatchSession() {
  98. if WCSession.isSupported() {
  99. let session = WCSession.default
  100. session.delegate = self
  101. session.activate()
  102. self.session = session
  103. debug(.watchManager, "📱 Phone session setup - isPaired: \(session.isPaired)")
  104. } else {
  105. debug(.watchManager, "📱 WCSession is not supported on this device")
  106. }
  107. }
  108. /// Attempts to reestablish the Watch connection if it becomes unreachable
  109. private func retryConnection() {
  110. guard let session = session else { return }
  111. if !session.isReachable {
  112. debug(.watchManager, "📱 Attempting to reactivate session...")
  113. session.activate()
  114. }
  115. }
  116. /// Prepares the current state data to be sent to the Watch
  117. /// - Returns: WatchState containing current glucose readings and trends and determination infos for displaying cob and iob in the view
  118. private func setupWatchState() async -> WatchState {
  119. // Get NSManagedObjectIDs
  120. let glucoseIds = await fetchGlucose()
  121. // TODO: - if we want that the watch immediately displays updated cob and iob values when entered via treatment view from phone, we would need to use a predicate here that also filters for NON-ENACTED Determinations
  122. let determinationIds = await determinationStorage.fetchLastDeterminationObjectID(
  123. predicate: NSPredicate.predicateFor30MinAgoForDetermination
  124. )
  125. let overridePresetIds = await overrideStorage.fetchForOverridePresets()
  126. let tempTargetPresetIds = await tempTargetStorage.fetchForTempTargetPresets()
  127. // Get NSManagedObjects
  128. let glucoseObjects: [GlucoseStored] = await CoreDataStack.shared
  129. .getNSManagedObject(with: glucoseIds, context: backgroundContext)
  130. let determinationObjects: [OrefDetermination] = await CoreDataStack.shared
  131. .getNSManagedObject(with: determinationIds, context: backgroundContext)
  132. let overridePresetObjects: [OverrideStored] = await CoreDataStack.shared
  133. .getNSManagedObject(with: overridePresetIds, context: backgroundContext)
  134. let tempTargetPresetObjects: [TempTargetStored] = await CoreDataStack.shared
  135. .getNSManagedObject(with: tempTargetPresetIds, context: backgroundContext)
  136. return await backgroundContext.perform {
  137. var watchState = WatchState()
  138. // Set lastLoopDate
  139. let lastLoopMinutes = Int((Date().timeIntervalSince(self.apsManager.lastLoopDate) - 30) / 60) + 1
  140. if lastLoopMinutes > 1440 {
  141. watchState.lastLoopTime = "--"
  142. } else {
  143. watchState.lastLoopTime = "\(lastLoopMinutes) min"
  144. }
  145. // Set IOB and COB from latest determination
  146. if let latestDetermination = determinationObjects.first {
  147. let iob = latestDetermination.iob ?? 0
  148. watchState.iob = Formatter.decimalFormatterWithTwoFractionDigits.string(from: iob)
  149. let cob = NSNumber(value: latestDetermination.cob)
  150. watchState.cob = Formatter.integerFormatter.string(from: cob)
  151. }
  152. // Set override presets with their enabled status
  153. watchState.overridePresets = overridePresetObjects.map { override in
  154. OverridePresetWatch(
  155. name: override.name ?? "",
  156. isEnabled: override.enabled
  157. )
  158. }
  159. guard let latestGlucose = glucoseObjects.first else {
  160. return watchState
  161. }
  162. // Map glucose values
  163. watchState.glucoseValues = glucoseObjects.compactMap { glucose in
  164. var glucoseValue: Double
  165. if self.units == .mgdL {
  166. glucoseValue = Double(glucose.glucose)
  167. } else {
  168. let mgdlValue = Decimal(glucose.glucose)
  169. glucoseValue = Double(truncating: mgdlValue.asMmolL as NSNumber)
  170. }
  171. // TODO: workaround for now: set low value to 55, to have dynamic color shades between 55 and user-set low (approx. 70); same for high glucose
  172. let hardCodedLow = Decimal(55)
  173. let hardCodedHigh = Decimal(220)
  174. let isDynamicColorScheme = self.glucoseColorScheme == .dynamicColor
  175. let highGlucoseValue = isDynamicColorScheme ? hardCodedHigh : self.highGlucose
  176. let lowGlucoseValue = isDynamicColorScheme ? hardCodedLow : self.lowGlucose
  177. let highGlucoseColorValue = self.units == .mgdL ? highGlucoseValue : highGlucoseValue.asMmolL
  178. let lowGlucoseColorValue = self.units == .mgdL ? lowGlucoseValue : lowGlucoseValue.asMmolL
  179. let targetGlucose = self.units == .mgdL ? self.currentGlucoseTarget : self.currentGlucoseTarget.asMmolL
  180. let glucoseColor = Trio.getDynamicGlucoseColor(
  181. glucoseValue: Decimal(glucose.glucose),
  182. highGlucoseColorValue: highGlucoseColorValue,
  183. lowGlucoseColorValue: lowGlucoseColorValue,
  184. targetGlucose: targetGlucose,
  185. glucoseColorScheme: self.glucoseColorScheme
  186. )
  187. return (date: glucose.date ?? Date(), glucose: glucoseValue, color: glucoseColor)
  188. }
  189. .sorted { $0.date < $1.date }
  190. // Set current glucose with proper formatting
  191. if self.units == .mgdL {
  192. watchState.currentGlucose = "\(latestGlucose.glucose)"
  193. } else {
  194. let mgdlValue = Decimal(latestGlucose.glucose)
  195. let latestGlucoseValue = Double(truncating: mgdlValue.asMmolL as NSNumber)
  196. watchState.currentGlucose = "\(latestGlucoseValue)"
  197. }
  198. // Convert direction to trend string
  199. watchState.trend = latestGlucose.direction
  200. // Calculate delta if we have at least 2 readings
  201. if glucoseObjects.count >= 2 {
  202. let deltaValue = glucoseObjects[0].glucose - glucoseObjects[1].glucose
  203. let formattedDelta = Formatter.glucoseFormatter(for: self.units)
  204. .string(from: NSNumber(value: abs(deltaValue))) ?? "0"
  205. watchState.delta = deltaValue < 0 ? "-\(formattedDelta)" : "+\(formattedDelta)"
  206. }
  207. // Set temp target presets with their enabled status
  208. watchState.tempTargetPresets = tempTargetPresetObjects.map { tempTarget in
  209. TempTargetPresetWatch(
  210. name: tempTarget.name ?? "",
  211. isEnabled: tempTarget.enabled
  212. )
  213. }
  214. // Set units
  215. watchState.units = self.units
  216. // Add settings values
  217. watchState.maxBolus = self.settingsManager.pumpSettings.maxBolus
  218. watchState.maxCarbs = self.settingsManager.settings.maxCarbs
  219. watchState.maxFat = self.settingsManager.settings.maxFat
  220. watchState.maxProtein = self.settingsManager.settings.maxProtein
  221. watchState.maxIOB = self.settingsManager.preferences.maxIOB
  222. watchState.maxCOB = self.settingsManager.preferences.maxCOB
  223. debug(
  224. .watchManager,
  225. "📱 Setup WatchState - currentGlucose: \(watchState.currentGlucose ?? "nil"), trend: \(watchState.trend ?? "nil"), delta: \(watchState.delta ?? "nil"), values: \(watchState.glucoseValues.count)"
  226. )
  227. return watchState
  228. }
  229. }
  230. /// Fetches recent glucose readings from CoreData
  231. /// - Returns: Array of NSManagedObjectIDs for glucose readings
  232. private func fetchGlucose() async -> [NSManagedObjectID] {
  233. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  234. ofType: GlucoseStored.self,
  235. onContext: backgroundContext,
  236. predicate: NSPredicate.glucose,
  237. key: "date",
  238. ascending: false,
  239. fetchLimit: 288
  240. )
  241. return await backgroundContext.perform {
  242. guard let fetchedResults = results as? [GlucoseStored] else { return [] }
  243. return fetchedResults.map(\.objectID)
  244. }
  245. }
  246. // MARK: - Send to Watch
  247. /// Sends the state of type WatchState to the connected Watch
  248. /// - Parameter state: Current WatchState containing glucose data to be sent
  249. func sendDataToWatch(_ state: WatchState) {
  250. guard let session = session, session.isReachable else {
  251. debug(.watchManager, "⌚️ Watch not reachable")
  252. return
  253. }
  254. let message: [String: Any] = [
  255. "currentGlucose": state.currentGlucose ?? "",
  256. "trend": state.trend ?? "",
  257. "delta": state.delta ?? "",
  258. "iob": state.iob ?? "",
  259. "cob": state.cob ?? "",
  260. "lastLoopTime": state.lastLoopTime ?? "",
  261. "glucoseValues": state.glucoseValues.map { value in
  262. [
  263. "glucose": value.glucose,
  264. "date": value.date.timeIntervalSince1970,
  265. "color": value.color
  266. ]
  267. },
  268. "overridePresets": state.overridePresets.map { preset in
  269. [
  270. "name": preset.name,
  271. "isEnabled": preset.isEnabled
  272. ]
  273. },
  274. "tempTargetPresets": state.tempTargetPresets.map { preset in
  275. [
  276. "name": preset.name,
  277. "isEnabled": preset.isEnabled
  278. ]
  279. },
  280. "maxBolus": state.maxBolus,
  281. "maxCarbs": state.maxCarbs,
  282. "maxFat": state.maxFat,
  283. "maxProtein": state.maxProtein,
  284. "maxIOB": state.maxIOB,
  285. "maxCOB": state.maxCOB
  286. ]
  287. debug(.watchManager, "📱 Sending to watch - Message content:")
  288. message.forEach { key, value in
  289. debug(.watchManager, "📱 \(key): \(value) (type: \(type(of: value)))")
  290. }
  291. session.sendMessage(message, replyHandler: nil) { error in
  292. debug(.watchManager, "❌ Error sending data: \(error.localizedDescription)")
  293. }
  294. }
  295. func sendAcknowledgment(toWatch success: Bool, message: String = "") {
  296. guard let session = session, session.isReachable else {
  297. debug(.watchManager, "⌚️ Watch not reachable for acknowledgment")
  298. return
  299. }
  300. let ackMessage: [String: Any] = [
  301. "acknowledged": success,
  302. "message": message
  303. ]
  304. session.sendMessage(ackMessage, replyHandler: nil) { error in
  305. debug(.watchManager, "❌ Error sending acknowledgment: \(error.localizedDescription)")
  306. }
  307. }
  308. // MARK: - WCSessionDelegate
  309. func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
  310. if let error = error {
  311. debug(.watchManager, "📱 Phone session activation failed: \(error.localizedDescription)")
  312. return
  313. }
  314. debug(.watchManager, "📱 Phone session activated with state: \(activationState.rawValue)")
  315. debug(.watchManager, "📱 Phone isReachable after activation: \(session.isReachable)")
  316. // Try to send initial data after activation
  317. Task {
  318. let state = await self.setupWatchState()
  319. self.sendDataToWatch(state)
  320. }
  321. }
  322. func session(_: WCSession, didReceiveMessage message: [String: Any]) {
  323. DispatchQueue.main.async { [weak self] in
  324. if let bolusAmount = message["bolus"] as? Double,
  325. message["carbs"] == nil,
  326. message["date"] == nil
  327. {
  328. debug(.watchManager, "📱 Received bolus request from watch: \(bolusAmount)U")
  329. self?.handleBolusRequest(Decimal(bolusAmount))
  330. } else if let carbsAmount = message["carbs"] as? Int,
  331. let timestamp = message["date"] as? TimeInterval,
  332. message["bolus"] == nil
  333. {
  334. let date = Date(timeIntervalSince1970: timestamp)
  335. debug(.watchManager, "📱 Received carbs request from watch: \(carbsAmount)g at \(date)")
  336. self?.handleCarbsRequest(carbsAmount, date)
  337. } else if let bolusAmount = message["bolus"] as? Double,
  338. let carbsAmount = message["carbs"] as? Int,
  339. let timestamp = message["date"] as? TimeInterval
  340. {
  341. let date = Date(timeIntervalSince1970: timestamp)
  342. debug(
  343. .watchManager,
  344. "📱 Received meal bolus combo request from watch: \(bolusAmount)U, \(carbsAmount)g at \(date)"
  345. )
  346. self?.handleCombinedRequest(bolusAmount: Decimal(bolusAmount), carbsAmount: Decimal(carbsAmount), date: date)
  347. } else {
  348. debug(.watchManager, "📱 Invalid or incomplete data received from watch. Received: \(message)")
  349. }
  350. if message["cancelOverride"] as? Bool == true {
  351. debug(.watchManager, "📱 Received cancel override request from watch")
  352. self?.handleCancelOverride()
  353. }
  354. if let presetName = message["activateOverride"] as? String {
  355. debug(.watchManager, "📱 Received activate override request from watch for preset: \(presetName)")
  356. self?.handleActivateOverride(presetName)
  357. }
  358. if let presetName = message["activateTempTarget"] as? String {
  359. debug(.watchManager, "📱 Received activate temp target request from watch for preset: \(presetName)")
  360. self?.handleActivateTempTarget(presetName)
  361. }
  362. if message["cancelTempTarget"] as? Bool == true {
  363. debug(.watchManager, "📱 Received cancel temp target request from watch")
  364. self?.handleCancelTempTarget()
  365. }
  366. // Handle bolus cancellation
  367. if message["cancelBolus"] as? Bool == true {
  368. Task {
  369. await self?.apsManager.cancelBolus { [self] success, message in
  370. // Acknowledge success or error of bolus
  371. self?.sendAcknowledgment(toWatch: success, message: message)
  372. }
  373. debug(.watchManager, "📱 Bolus cancelled from watch")
  374. // perform determine basal sync, otherwise you have could end up with too much iob when opening the calculator again
  375. await self?.apsManager.determineBasalSync()
  376. }
  377. }
  378. }
  379. }
  380. #if os(iOS)
  381. func sessionDidBecomeInactive(_: WCSession) {}
  382. func sessionDidDeactivate(_ session: WCSession) {
  383. session.activate()
  384. }
  385. #endif
  386. func sessionReachabilityDidChange(_ session: WCSession) {
  387. debug(.watchManager, "📱 Phone reachability changed: \(session.isReachable)")
  388. if session.isReachable {
  389. // Try to send data when connection is established
  390. Task {
  391. let state = await self.setupWatchState()
  392. self.sendDataToWatch(state)
  393. }
  394. } else {
  395. // Try to reconnect after a short delay
  396. DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
  397. self?.retryConnection()
  398. }
  399. }
  400. }
  401. /// Processes bolus requests received from the Watch
  402. /// - Parameter amount: The requested bolus amount in units
  403. private func handleBolusRequest(_ amount: Decimal) {
  404. Task {
  405. await apsManager.enactBolus(amount: Double(amount), isSMB: false) { success, message in
  406. // Acknowledge success or error of bolus
  407. self.sendAcknowledgment(toWatch: success, message: message)
  408. }
  409. debug(.watchManager, "📱 Enacted bolus via APS Manager: \(amount)U")
  410. }
  411. }
  412. /// Handles carbs entry requests received from the Watch
  413. /// - Parameters:
  414. /// - amount: The carbs amount in grams
  415. /// - date: Timestamp for the carbs entry
  416. private func handleCarbsRequest(_ amount: Int, _ date: Date) {
  417. Task {
  418. let context = CoreDataStack.shared.newTaskContext()
  419. await context.perform {
  420. let carbs = CarbEntryStored(context: context)
  421. carbs.carbs = Double(truncating: amount as NSNumber)
  422. carbs.date = date
  423. carbs.id = UUID()
  424. carbs.isFPU = false
  425. // TODO: add FPU
  426. do {
  427. guard context.hasChanges else { return }
  428. try context.save()
  429. debug(.watchManager, "📱 Saved carbs from watch: \(amount)g at \(date)")
  430. // Acknowledge success
  431. self.sendAcknowledgment(toWatch: true, message: "Carbs logged successfully.")
  432. } catch {
  433. debug(.watchManager, "❌ Error saving carbs: \(error.localizedDescription)")
  434. // Acknowledge failure
  435. self.sendAcknowledgment(toWatch: false, message: "Error logging carbs")
  436. }
  437. }
  438. }
  439. }
  440. /// Handles combined bolus and carbs entry requests received from the Watch.
  441. /// - Parameters:
  442. /// - bolusAmount: The bolus amount in units
  443. /// - carbsAmount: The carbs amount in grams
  444. /// - date: Timestamp for the carbs entry
  445. private func handleCombinedRequest(bolusAmount: Decimal, carbsAmount: Decimal, date: Date) {
  446. Task {
  447. let context = CoreDataStack.shared.newTaskContext()
  448. do {
  449. // Notify Watch: "Saving carbs..."
  450. self.sendAcknowledgment(toWatch: true, message: "Saving Carbs...")
  451. // Save carbs entry in Core Data
  452. try await context.perform {
  453. let carbEntry = CarbEntryStored(context: context)
  454. carbEntry.carbs = NSDecimalNumber(decimal: carbsAmount).doubleValue
  455. carbEntry.date = date
  456. // TODO: Add Fat-Protein Units (FPU) logic if required
  457. guard context.hasChanges else { return }
  458. try context.save()
  459. debug(.watchManager, "📱 Saved carbs from watch: \(carbsAmount) g at \(date)")
  460. }
  461. // Notify Watch: "Enacting bolus..."
  462. sendAcknowledgment(toWatch: true, message: "Enacting bolus...")
  463. // Enact bolus via APS Manager
  464. let bolusDouble = NSDecimalNumber(decimal: bolusAmount).doubleValue
  465. await apsManager.enactBolus(amount: bolusDouble, isSMB: false) { success, message in
  466. // Acknowledge success or error of bolus
  467. self.sendAcknowledgment(toWatch: success, message: message)
  468. }
  469. debug(.watchManager, "📱 Enacted bolus from watch via APS Manager: \(bolusDouble) U")
  470. // Notify Watch: "Carbs and bolus logged successfully"
  471. sendAcknowledgment(toWatch: true, message: "Carbs and Bolus logged successfully.")
  472. } catch {
  473. debug(.watchManager, "❌ Error processing combined request: \(error.localizedDescription)")
  474. sendAcknowledgment(toWatch: false, message: "Failed to log carbs and bolus")
  475. }
  476. }
  477. }
  478. private func handleCancelOverride() {
  479. Task {
  480. let context = CoreDataStack.shared.newTaskContext()
  481. if let overrideId = await overrideStorage.fetchLatestActiveOverride() {
  482. let override = await context.perform {
  483. context.object(with: overrideId) as? OverrideStored
  484. }
  485. await context.perform {
  486. if let activeOverride = override {
  487. activeOverride.enabled = false
  488. do {
  489. guard context.hasChanges else { return }
  490. try context.save()
  491. debug(.watchManager, "📱 Successfully stopped override")
  492. // Send notification to update Adjustments UI
  493. Foundation.NotificationCenter.default.post(
  494. name: .didUpdateOverrideConfiguration,
  495. object: nil
  496. )
  497. // Acknowledge cancellation success
  498. self.sendAcknowledgment(toWatch: true, message: "Stopped Override successfully.")
  499. } catch {
  500. debug(.watchManager, "❌ Error cancelling override: \(error.localizedDescription)")
  501. // Acknowledge cancellation error
  502. self.sendAcknowledgment(toWatch: false, message: "Error stopping Override.")
  503. }
  504. }
  505. }
  506. }
  507. }
  508. }
  509. private func handleActivateOverride(_ presetName: String) {
  510. Task {
  511. let context = CoreDataStack.shared.newTaskContext()
  512. // Fetch all presets to find the one to activate
  513. let presetIds = await overrideStorage.fetchForOverridePresets()
  514. let presets: [OverrideStored] = await CoreDataStack.shared
  515. .getNSManagedObject(with: presetIds, context: context)
  516. // Check for active override
  517. if let activeOverrideId = await overrideStorage.fetchLatestActiveOverride() {
  518. let activeOverride = await context.perform {
  519. context.object(with: activeOverrideId) as? OverrideStored
  520. }
  521. // Deactivate if exists
  522. if let override = activeOverride {
  523. await context.perform {
  524. override.enabled = false
  525. }
  526. }
  527. }
  528. // Activate the selected preset
  529. await context.perform {
  530. if let presetToActivate = presets.first(where: { $0.name == presetName }) {
  531. presetToActivate.enabled = true
  532. presetToActivate.date = Date()
  533. do {
  534. guard context.hasChanges else { return }
  535. try context.save()
  536. debug(.watchManager, "📱 Successfully activated override: \(presetName)")
  537. // Send notification to update Adjustments UI
  538. Foundation.NotificationCenter.default.post(
  539. name: .didUpdateOverrideConfiguration,
  540. object: nil
  541. )
  542. // Acknowledge activation success
  543. self.sendAcknowledgment(toWatch: true, message: "Started Override \"\(presetName)\" successfully.")
  544. } catch {
  545. debug(.watchManager, "❌ Error activating override: \(error.localizedDescription)")
  546. // Acknowledge activation error
  547. self.sendAcknowledgment(toWatch: false, message: "Error activating Override \"\(presetName)\".")
  548. }
  549. }
  550. }
  551. }
  552. }
  553. private func handleActivateTempTarget(_ presetName: String) {
  554. Task {
  555. let context = CoreDataStack.shared.newTaskContext()
  556. // Fetch all presets to find the one to activate
  557. let presetIds = await tempTargetStorage.fetchForTempTargetPresets()
  558. let presets: [TempTargetStored] = await CoreDataStack.shared
  559. .getNSManagedObject(with: presetIds, context: context)
  560. // Check for active temp target
  561. if let activeTempTargetId = await tempTargetStorage.loadLatestTempTargetConfigurations(fetchLimit: 1).first {
  562. let activeTempTarget = await context.perform {
  563. context.object(with: activeTempTargetId) as? TempTargetStored
  564. }
  565. // Deactivate if exists
  566. if let tempTarget = activeTempTarget {
  567. await context.perform {
  568. tempTarget.enabled = false
  569. }
  570. }
  571. }
  572. // Activate the selected preset
  573. await context.perform {
  574. if let presetToActivate = presets.first(where: { $0.name == presetName }) {
  575. presetToActivate.enabled = true
  576. presetToActivate.date = Date()
  577. do {
  578. guard context.hasChanges else { return }
  579. try context.save()
  580. debug(.watchManager, "📱 Successfully activated temp target: \(presetName)")
  581. let settingsHalfBasalTarget = self.settingsManager.preferences
  582. .halfBasalExerciseTarget
  583. let halfBasalTarget = presetToActivate.halfBasalTarget?.decimalValue
  584. // To activate the temp target also in oref
  585. let tempTarget = TempTarget(
  586. name: presetToActivate.name,
  587. createdAt: Date(),
  588. targetTop: presetToActivate.target?.decimalValue,
  589. targetBottom: presetToActivate.target?.decimalValue,
  590. duration: presetToActivate.duration?.decimalValue ?? 0,
  591. enteredBy: TempTarget.local,
  592. reason: TempTarget.custom,
  593. isPreset: true,
  594. enabled: true,
  595. halfBasalTarget: halfBasalTarget ?? settingsHalfBasalTarget
  596. )
  597. self.tempTargetStorage.saveTempTargetsToStorage([tempTarget])
  598. // Send notification to update Adjustments UI
  599. Foundation.NotificationCenter.default.post(
  600. name: .didUpdateTempTargetConfiguration,
  601. object: nil
  602. )
  603. // Acknowledge activation success
  604. self.sendAcknowledgment(toWatch: true, message: "Started Temp Target \"\(presetName)\" successfully.")
  605. } catch {
  606. debug(.watchManager, "❌ Error activating temp target: \(error.localizedDescription)")
  607. // Acknowledge activation error
  608. self.sendAcknowledgment(toWatch: false, message: "Error activating Temp Target \"\(presetName)\".")
  609. }
  610. }
  611. }
  612. }
  613. }
  614. private func handleCancelTempTarget() {
  615. Task {
  616. let context = CoreDataStack.shared.newTaskContext()
  617. if let tempTargetId = await tempTargetStorage.loadLatestTempTargetConfigurations(fetchLimit: 1).first {
  618. let tempTarget = await context.perform {
  619. context.object(with: tempTargetId) as? TempTargetStored
  620. }
  621. await context.perform {
  622. if let activeTempTarget = tempTarget {
  623. activeTempTarget.enabled = false
  624. do {
  625. guard context.hasChanges else { return }
  626. try context.save()
  627. debug(.watchManager, "📱 Successfully cancelled temp target")
  628. // To cancel the temp target also for oref
  629. self.tempTargetStorage.saveTempTargetsToStorage([TempTarget.cancel(at: Date())])
  630. // Send notification to update Adjustments UI
  631. Foundation.NotificationCenter.default.post(
  632. name: .didUpdateTempTargetConfiguration,
  633. object: nil
  634. )
  635. // Acknowledge cancellation success
  636. self.sendAcknowledgment(toWatch: true, message: "Stopped Temp Target successfully.")
  637. } catch {
  638. debug(.watchManager, "❌ Error stopping temp target: \(error.localizedDescription)")
  639. // Acknowledge cancellation error
  640. self.sendAcknowledgment(toWatch: false, message: "Error stopping Temp Target.")
  641. }
  642. }
  643. }
  644. }
  645. }
  646. }
  647. /// Subscribes to bolus progress updates and sends progress or cancellation messages to the Watch
  648. private func subscribeToBolusProgress() {
  649. var wasBolusActive = false
  650. apsManager.bolusProgress
  651. .receive(on: DispatchQueue.main)
  652. .sink { [weak self] progress in
  653. if let progress = progress {
  654. wasBolusActive = true
  655. self?.sendBolusProgressToWatch(progress: progress)
  656. } else if wasBolusActive {
  657. // Only if a bolus was previously active and now nil is received,
  658. // the bolus was cancelled
  659. wasBolusActive = false
  660. debug(.watchManager, "📱 Bolus cancelled from phone")
  661. self?.sendBolusCanceledMessageToWatch()
  662. }
  663. }
  664. .store(in: &subscriptions)
  665. }
  666. /// Sends bolus progress updates to the Watch
  667. /// - Parameter progress: The current bolus progress as a Decimal
  668. private func sendBolusProgressToWatch(progress: Decimal?) {
  669. guard let session = session, session.isReachable, let progress = progress else { return }
  670. let message: [String: Any] = ["bolusProgress": Double(truncating: progress as NSNumber)]
  671. session.sendMessage(message, replyHandler: nil) { error in
  672. debug(.watchManager, "❌ Error sending bolus progress: \(error.localizedDescription)")
  673. }
  674. }
  675. private func sendBolusCanceledMessageToWatch() {
  676. if let session = session, session.isReachable {
  677. let message: [String: Any] = ["bolusCanceled": true]
  678. session.sendMessage(message, replyHandler: nil) { error in
  679. debug(.watchManager, "❌ Error sending bolus cancellation to watch: \(error.localizedDescription)")
  680. }
  681. }
  682. }
  683. }
  684. // TODO: - is there a better approach than setting up the watch state every time a setting has changed?
  685. extension BaseWatchManager: SettingsObserver, PumpSettingsObserver, PreferencesObserver {
  686. // to update maxCOB, maxIOB
  687. func preferencesDidChange(_: Preferences) {
  688. Task {
  689. let state = await self.setupWatchState()
  690. self.sendDataToWatch(state)
  691. }
  692. }
  693. // to update maxBolus
  694. func pumpSettingsDidChange(_: PumpSettings) {
  695. Task {
  696. let state = await self.setupWatchState()
  697. self.sendDataToWatch(state)
  698. }
  699. }
  700. // to update the rest
  701. func settingsDidChange(_: TrioSettings) {
  702. units = settingsManager.settings.units
  703. glucoseColorScheme = settingsManager.settings.glucoseColorScheme
  704. lowGlucose = settingsManager.settings.low
  705. highGlucose = settingsManager.settings.high
  706. Task {
  707. let state = await self.setupWatchState()
  708. self.sendDataToWatch(state)
  709. }
  710. }
  711. }
  712. extension BaseWatchManager {
  713. /// Retrieves the current glucose target based on the time of day.
  714. private func getCurrentGlucoseTarget() async -> Decimal? {
  715. let now = Date()
  716. let calendar = Calendar.current
  717. let dateFormatter = DateFormatter()
  718. dateFormatter.dateFormat = "HH:mm"
  719. dateFormatter.timeZone = TimeZone.current
  720. let bgTargets = await fileStorage.retrieveAsync(OpenAPS.Settings.bgTargets, as: BGTargets.self)
  721. ?? BGTargets(from: OpenAPS.defaults(for: OpenAPS.Settings.bgTargets))
  722. ?? BGTargets(units: .mgdL, userPreferredUnits: .mgdL, targets: [])
  723. let entries: [(start: String, value: Decimal)] = bgTargets.targets.map { ($0.start, $0.low) }
  724. for (index, entry) in entries.enumerated() {
  725. guard let entryTime = dateFormatter.date(from: entry.start) else {
  726. print("Invalid entry start time: \(entry.start)")
  727. continue
  728. }
  729. let entryComponents = calendar.dateComponents([.hour, .minute, .second], from: entryTime)
  730. let entryStartTime = calendar.date(
  731. bySettingHour: entryComponents.hour!,
  732. minute: entryComponents.minute!,
  733. second: entryComponents.second!,
  734. of: now
  735. )!
  736. let entryEndTime: Date
  737. if index < entries.count - 1,
  738. let nextEntryTime = dateFormatter.date(from: entries[index + 1].start)
  739. {
  740. let nextEntryComponents = calendar.dateComponents([.hour, .minute, .second], from: nextEntryTime)
  741. entryEndTime = calendar.date(
  742. bySettingHour: nextEntryComponents.hour!,
  743. minute: nextEntryComponents.minute!,
  744. second: nextEntryComponents.second!,
  745. of: now
  746. )!
  747. } else {
  748. entryEndTime = calendar.date(byAdding: .day, value: 1, to: entryStartTime)!
  749. }
  750. if now >= entryStartTime, now < entryEndTime {
  751. return entry.value
  752. }
  753. }
  754. return nil
  755. }
  756. }