AppleWatchManager.swift 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733
  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 glucoseStorage: GlucoseStorage!
  15. @Injected() private var apsManager: APSManager!
  16. @Injected() private var settingsManager: SettingsManager!
  17. @Injected() private var determinationStorage: DeterminationStorage!
  18. @Injected() private var overrideStorage: OverrideStorage!
  19. @Injected() private var tempTargetStorage: TempTargetsStorage!
  20. private var units: GlucoseUnits = .mgdL
  21. private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
  22. private var subscriptions = Set<AnyCancellable>()
  23. typealias PumpEvent = PumpEventStored.EventType
  24. let backgroundContext = CoreDataStack.shared.newTaskContext()
  25. let viewContext = CoreDataStack.shared.persistentContainer.viewContext
  26. init(resolver: Resolver) {
  27. super.init()
  28. injectServices(resolver)
  29. setupWatchSession()
  30. units = settingsManager.settings.units
  31. broadcaster.register(SettingsObserver.self, observer: self)
  32. broadcaster.register(PumpSettingsObserver.self, observer: self)
  33. broadcaster.register(PreferencesObserver.self, observer: self)
  34. // Observer for OrefDetermination and adjustments
  35. coreDataPublisher =
  36. changedObjectsOnManagedObjectContextDidSavePublisher()
  37. .receive(on: DispatchQueue.global(qos: .background))
  38. .share()
  39. .eraseToAnyPublisher()
  40. // Observer for glucose and manual glucose
  41. glucoseStorage.updatePublisher
  42. .receive(on: DispatchQueue.global(qos: .background))
  43. .sink { [weak self] _ in
  44. guard let self = self else { return }
  45. Task {
  46. let state = await self.setupWatchState()
  47. self.sendDataToWatch(state)
  48. }
  49. }
  50. .store(in: &subscriptions)
  51. registerHandlers()
  52. subscribeToBolusProgress()
  53. }
  54. private func registerHandlers() {
  55. coreDataPublisher?.filterByEntityName("OrefDetermination").sink { [weak self] _ in
  56. guard let self = self else { return }
  57. Task {
  58. let state = await self.setupWatchState()
  59. self.sendDataToWatch(state)
  60. }
  61. }.store(in: &subscriptions)
  62. // Due to the Batch insert this only is used for observing Deletion of Glucose entries
  63. coreDataPublisher?.filterByEntityName("GlucoseStored").sink { [weak self] _ in
  64. guard let self = self else { return }
  65. Task {
  66. let state = await self.setupWatchState()
  67. self.sendDataToWatch(state)
  68. }
  69. }.store(in: &subscriptions)
  70. coreDataPublisher?.filterByEntityName("OverrideStored").sink { [weak self] _ in
  71. guard let self = self else { return }
  72. Task {
  73. let state = await self.setupWatchState()
  74. self.sendDataToWatch(state)
  75. }
  76. }.store(in: &subscriptions)
  77. coreDataPublisher?.filterByEntityName("TempTargetStored").sink { [weak self] _ in
  78. guard let self = self else { return }
  79. Task {
  80. let state = await self.setupWatchState()
  81. self.sendDataToWatch(state)
  82. }
  83. }.store(in: &subscriptions)
  84. }
  85. /// Sets up the WatchConnectivity session if the device supports it
  86. private func setupWatchSession() {
  87. if WCSession.isSupported() {
  88. let session = WCSession.default
  89. session.delegate = self
  90. session.activate()
  91. self.session = session
  92. debug(.watchManager, "📱 Phone session setup - isPaired: \(session.isPaired)")
  93. } else {
  94. debug(.watchManager, "📱 WCSession is not supported on this device")
  95. }
  96. }
  97. /// Attempts to reestablish the Watch connection if it becomes unreachable
  98. private func retryConnection() {
  99. guard let session = session else { return }
  100. if !session.isReachable {
  101. debug(.watchManager, "📱 Attempting to reactivate session...")
  102. session.activate()
  103. }
  104. }
  105. /// Prepares the current state data to be sent to the Watch
  106. /// - Returns: WatchState containing current glucose readings and trends and determination infos for displaying cob and iob in the view
  107. private func setupWatchState() async -> WatchState {
  108. // Get NSManagedObjectIDs
  109. let glucoseIds = await fetchGlucose()
  110. // 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
  111. let determinationIds = await determinationStorage.fetchLastDeterminationObjectID(
  112. predicate: NSPredicate.predicateFor30MinAgoForDetermination
  113. )
  114. let overridePresetIds = await overrideStorage.fetchForOverridePresets()
  115. let tempTargetPresetIds = await tempTargetStorage.fetchForTempTargetPresets()
  116. // Get NSManagedObjects
  117. let glucoseObjects: [GlucoseStored] = await CoreDataStack.shared
  118. .getNSManagedObject(with: glucoseIds, context: backgroundContext)
  119. let determinationObjects: [OrefDetermination] = await CoreDataStack.shared
  120. .getNSManagedObject(with: determinationIds, context: backgroundContext)
  121. let overridePresetObjects: [OverrideStored] = await CoreDataStack.shared
  122. .getNSManagedObject(with: overridePresetIds, context: backgroundContext)
  123. let tempTargetPresetObjects: [TempTargetStored] = await CoreDataStack.shared
  124. .getNSManagedObject(with: tempTargetPresetIds, context: backgroundContext)
  125. return await backgroundContext.perform {
  126. var watchState = WatchState()
  127. // Set lastLoopDate
  128. let lastLoopMinutes = Int((Date().timeIntervalSince(self.apsManager.lastLoopDate) - 30) / 60) + 1
  129. if lastLoopMinutes > 1440 {
  130. watchState.lastLoopTime = "--"
  131. } else {
  132. watchState.lastLoopTime = "\(lastLoopMinutes)m"
  133. }
  134. // Set IOB and COB from latest determination
  135. if let latestDetermination = determinationObjects.first {
  136. let iob = latestDetermination.iob ?? 0
  137. watchState.iob = Formatter.decimalFormatterWithTwoFractionDigits.string(from: iob)
  138. let cob = NSNumber(value: latestDetermination.cob)
  139. watchState.cob = Formatter.integerFormatter.string(from: cob)
  140. }
  141. // Set override presets with their enabled status
  142. watchState.overridePresets = overridePresetObjects.map { override in
  143. OverridePresetWatch(
  144. name: override.name ?? "",
  145. isEnabled: override.enabled
  146. )
  147. }
  148. guard let latestGlucose = glucoseObjects.first else {
  149. return watchState
  150. }
  151. // Map glucose values
  152. watchState.glucoseValues = glucoseObjects.compactMap { glucose in
  153. guard let date = glucose.date else { return nil }
  154. return (date: date, glucose: Double(glucose.glucose))
  155. }
  156. .sorted { $0.date < $1.date }
  157. // Set current glucose with proper formatting
  158. watchState.currentGlucose = "\(latestGlucose.glucose)"
  159. // Convert direction to trend string
  160. watchState.trend = latestGlucose.direction
  161. // Calculate delta if we have at least 2 readings
  162. if glucoseObjects.count >= 2 {
  163. let deltaValue = glucoseObjects[0].glucose - glucoseObjects[1].glucose
  164. let formattedDelta = Formatter.glucoseFormatter(for: self.units)
  165. .string(from: NSNumber(value: abs(deltaValue))) ?? "0"
  166. watchState.delta = deltaValue < 0 ? "-\(formattedDelta)" : "+\(formattedDelta)"
  167. }
  168. // Set temp target presets with their enabled status
  169. watchState.tempTargetPresets = tempTargetPresetObjects.map { tempTarget in
  170. TempTargetPresetWatch(
  171. name: tempTarget.name ?? "",
  172. isEnabled: tempTarget.enabled
  173. )
  174. }
  175. // Set units
  176. watchState.units = self.units
  177. // Add settings values
  178. watchState.maxBolus = self.settingsManager.pumpSettings.maxBolus
  179. watchState.maxCarbs = self.settingsManager.settings.maxCarbs
  180. watchState.maxFat = self.settingsManager.settings.maxFat
  181. watchState.maxProtein = self.settingsManager.settings.maxProtein
  182. watchState.maxIOB = self.settingsManager.preferences.maxIOB
  183. watchState.maxCOB = self.settingsManager.preferences.maxCOB
  184. debug(
  185. .watchManager,
  186. "📱 Setup WatchState - currentGlucose: \(watchState.currentGlucose ?? "nil"), trend: \(watchState.trend ?? "nil"), delta: \(watchState.delta ?? "nil"), values: \(watchState.glucoseValues.count)"
  187. )
  188. return watchState
  189. }
  190. }
  191. /// Fetches recent glucose readings from CoreData
  192. /// - Returns: Array of NSManagedObjectIDs for glucose readings
  193. private func fetchGlucose() async -> [NSManagedObjectID] {
  194. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  195. ofType: GlucoseStored.self,
  196. onContext: backgroundContext,
  197. predicate: NSPredicate.glucose,
  198. key: "date",
  199. ascending: false,
  200. fetchLimit: 288
  201. )
  202. return await backgroundContext.perform {
  203. guard let fetchedResults = results as? [GlucoseStored] else { return [] }
  204. return fetchedResults.map(\.objectID)
  205. }
  206. }
  207. // MARK: - Send to Watch
  208. /// Sends the state of type WatchState to the connected Watch
  209. /// - Parameter state: Current WatchState containing glucose data to be sent
  210. func sendDataToWatch(_ state: WatchState) {
  211. guard let session = session, session.isReachable else {
  212. debug(.watchManager, "⌚️ Watch not reachable")
  213. return
  214. }
  215. let message: [String: Any] = [
  216. "currentGlucose": state.currentGlucose ?? "",
  217. "trend": state.trend ?? "",
  218. "delta": state.delta ?? "",
  219. "iob": state.iob ?? "",
  220. "cob": state.cob ?? "",
  221. "lastLoopTime": state.lastLoopTime ?? "",
  222. "glucoseValues": state.glucoseValues.map { value in
  223. [
  224. "glucose": value.glucose,
  225. "date": value.date.timeIntervalSince1970
  226. ]
  227. },
  228. "overridePresets": state.overridePresets.map { preset in
  229. [
  230. "name": preset.name,
  231. "isEnabled": preset.isEnabled
  232. ]
  233. },
  234. "tempTargetPresets": state.tempTargetPresets.map { preset in
  235. [
  236. "name": preset.name,
  237. "isEnabled": preset.isEnabled
  238. ]
  239. },
  240. "maxBolus": state.maxBolus,
  241. "maxCarbs": state.maxCarbs,
  242. "maxFat": state.maxFat,
  243. "maxProtein": state.maxProtein,
  244. "maxIOB": state.maxIOB,
  245. "maxCOB": state.maxCOB
  246. ]
  247. debug(.watchManager, "📱 Sending to watch - Message content:")
  248. message.forEach { key, value in
  249. debug(.watchManager, "📱 \(key): \(value) (type: \(type(of: value)))")
  250. }
  251. session.sendMessage(message, replyHandler: nil) { error in
  252. debug(.watchManager, "❌ Error sending data: \(error.localizedDescription)")
  253. }
  254. }
  255. func sendAcknowledgment(toWatch success: Bool, message: String = "") {
  256. guard let session = session, session.isReachable else {
  257. debug(.watchManager, "⌚️ Watch not reachable for acknowledgment")
  258. return
  259. }
  260. let ackMessage: [String: Any] = [
  261. "acknowledged": success,
  262. "message": message
  263. ]
  264. session.sendMessage(ackMessage, replyHandler: nil) { error in
  265. debug(.watchManager, "❌ Error sending acknowledgment: \(error.localizedDescription)")
  266. }
  267. }
  268. // MARK: - WCSessionDelegate
  269. func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
  270. if let error = error {
  271. debug(.watchManager, "📱 Phone session activation failed: \(error.localizedDescription)")
  272. return
  273. }
  274. debug(.watchManager, "📱 Phone session activated with state: \(activationState.rawValue)")
  275. debug(.watchManager, "📱 Phone isReachable after activation: \(session.isReachable)")
  276. // Try to send initial data after activation
  277. Task {
  278. let state = await self.setupWatchState()
  279. self.sendDataToWatch(state)
  280. }
  281. }
  282. func session(_: WCSession, didReceiveMessage message: [String: Any]) {
  283. DispatchQueue.main.async { [weak self] in
  284. if let bolusAmount = message["bolus"] as? Double,
  285. message["carbs"] == nil,
  286. message["date"] == nil
  287. {
  288. debug(.watchManager, "📱 Received bolus request from watch: \(bolusAmount)U")
  289. self?.handleBolusRequest(Decimal(bolusAmount))
  290. } else if let carbsAmount = message["carbs"] as? Int,
  291. let timestamp = message["date"] as? TimeInterval,
  292. message["bolus"] == nil
  293. {
  294. let date = Date(timeIntervalSince1970: timestamp)
  295. debug(.watchManager, "📱 Received carbs request from watch: \(carbsAmount)g at \(date)")
  296. self?.handleCarbsRequest(carbsAmount, date)
  297. } else if let bolusAmount = message["bolus"] as? Double,
  298. let carbsAmount = message["carbs"] as? Int,
  299. let timestamp = message["date"] as? TimeInterval
  300. {
  301. let date = Date(timeIntervalSince1970: timestamp)
  302. debug(
  303. .watchManager,
  304. "📱 Received meal bolus combo request from watch: \(bolusAmount)U, \(carbsAmount)g at \(date)"
  305. )
  306. self?.handleCombinedRequest(bolusAmount: Decimal(bolusAmount), carbsAmount: Decimal(carbsAmount), date: date)
  307. } else {
  308. debug(.watchManager, "📱 Invalid or incomplete data received from watch. Received: \(message)")
  309. }
  310. if message["cancelOverride"] as? Bool == true {
  311. debug(.watchManager, "📱 Received cancel override request from watch")
  312. self?.handleCancelOverride()
  313. }
  314. if let presetName = message["activateOverride"] as? String {
  315. debug(.watchManager, "📱 Received activate override request from watch for preset: \(presetName)")
  316. self?.handleActivateOverride(presetName)
  317. }
  318. if let presetName = message["activateTempTarget"] as? String {
  319. debug(.watchManager, "📱 Received activate temp target request from watch for preset: \(presetName)")
  320. self?.handleActivateTempTarget(presetName)
  321. }
  322. if message["cancelTempTarget"] as? Bool == true {
  323. debug(.watchManager, "📱 Received cancel temp target request from watch")
  324. self?.handleCancelTempTarget()
  325. }
  326. // Handle bolus cancellation
  327. if message["cancelBolus"] as? Bool == true {
  328. Task {
  329. await self?.apsManager.cancelBolus()
  330. debug(.watchManager, "📱 Bolus cancelled from watch")
  331. }
  332. }
  333. }
  334. }
  335. #if os(iOS)
  336. func sessionDidBecomeInactive(_: WCSession) {}
  337. func sessionDidDeactivate(_ session: WCSession) {
  338. session.activate()
  339. }
  340. #endif
  341. func sessionReachabilityDidChange(_ session: WCSession) {
  342. debug(.watchManager, "📱 Phone reachability changed: \(session.isReachable)")
  343. if session.isReachable {
  344. // Try to send data when connection is established
  345. Task {
  346. let state = await self.setupWatchState()
  347. self.sendDataToWatch(state)
  348. }
  349. } else {
  350. // Try to reconnect after a short delay
  351. DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
  352. self?.retryConnection()
  353. }
  354. }
  355. }
  356. /// Processes bolus requests received from the Watch
  357. /// - Parameter amount: The requested bolus amount in units
  358. private func handleBolusRequest(_ amount: Decimal) {
  359. Task {
  360. await apsManager.enactBolus(amount: Double(amount), isSMB: false)
  361. debug(.watchManager, "📱 Enacted bolus via APS Manager: \(amount)U")
  362. // Acknowledge success
  363. self.sendAcknowledgment(toWatch: true, message: "Enacted bolus successfully")
  364. }
  365. }
  366. /// Handles carbs entry requests received from the Watch
  367. /// - Parameters:
  368. /// - amount: The carbs amount in grams
  369. /// - date: Timestamp for the carbs entry
  370. private func handleCarbsRequest(_ amount: Int, _ date: Date) {
  371. Task {
  372. let context = CoreDataStack.shared.newTaskContext()
  373. await context.perform {
  374. let carbs = CarbEntryStored(context: context)
  375. carbs.carbs = Double(truncating: amount as NSNumber)
  376. carbs.date = date
  377. carbs.id = UUID()
  378. // TODO: add FPU
  379. do {
  380. guard context.hasChanges else { return }
  381. try context.save()
  382. debug(.watchManager, "📱 Saved carbs from watch: \(amount)g at \(date)")
  383. // Acknowledge success
  384. self.sendAcknowledgment(toWatch: true, message: "Carbs logged successfully")
  385. } catch {
  386. debug(.watchManager, "❌ Error saving carbs: \(error.localizedDescription)")
  387. // Acknowledge failure
  388. self.sendAcknowledgment(toWatch: false, message: "Error logging carbs")
  389. }
  390. }
  391. }
  392. }
  393. /// Handles combined bolus and carbs entry requests received from the Watch.
  394. /// - Parameters:
  395. /// - bolusAmount: The bolus amount in units
  396. /// - carbsAmount: The carbs amount in grams
  397. /// - date: Timestamp for the carbs entry
  398. private func handleCombinedRequest(bolusAmount: Decimal, carbsAmount: Decimal, date: Date) {
  399. Task {
  400. let context = CoreDataStack.shared.newTaskContext()
  401. do {
  402. // Notify Watch: "Saving carbs..."
  403. self.sendAcknowledgment(toWatch: true, message: "Saving Carbs...")
  404. // Save carbs entry in Core Data
  405. try await context.perform {
  406. let carbEntry = CarbEntryStored(context: context)
  407. carbEntry.carbs = NSDecimalNumber(decimal: carbsAmount).doubleValue
  408. carbEntry.date = date
  409. // TODO: Add Fat-Protein Units (FPU) logic if required
  410. guard context.hasChanges else { return }
  411. try context.save()
  412. debug(.watchManager, "📱 Saved carbs from watch: \(carbsAmount) g at \(date)")
  413. }
  414. // Notify Watch: "Enacting bolus..."
  415. sendAcknowledgment(toWatch: true, message: "Enacting bolus...")
  416. // Enact bolus via APS Manager
  417. let bolusDouble = NSDecimalNumber(decimal: bolusAmount).doubleValue
  418. await apsManager.enactBolus(amount: bolusDouble, isSMB: false)
  419. debug(.watchManager, "📱 Enacted bolus from watch via APS Manager: \(bolusDouble) U")
  420. // Notify Watch: "Carbs and bolus logged successfully"
  421. sendAcknowledgment(toWatch: true, message: "Carbs and Bolus logged successfully")
  422. } catch {
  423. debug(.watchManager, "❌ Error processing combined request: \(error.localizedDescription)")
  424. sendAcknowledgment(toWatch: false, message: "Failed to log carbs and bolus")
  425. }
  426. }
  427. }
  428. private func handleCancelOverride() {
  429. Task {
  430. let context = CoreDataStack.shared.newTaskContext()
  431. if let overrideId = await overrideStorage.fetchLatestActiveOverride() {
  432. let override = await context.perform {
  433. context.object(with: overrideId) as? OverrideStored
  434. }
  435. await context.perform {
  436. if let activeOverride = override {
  437. activeOverride.enabled = false
  438. do {
  439. guard context.hasChanges else { return }
  440. try context.save()
  441. debug(.watchManager, "📱 Successfully cancelled override")
  442. // Send notification to update Adjustments UI
  443. Foundation.NotificationCenter.default.post(
  444. name: .didUpdateOverrideConfiguration,
  445. object: nil
  446. )
  447. } catch {
  448. debug(.watchManager, "❌ Error cancelling override: \(error.localizedDescription)")
  449. }
  450. }
  451. }
  452. }
  453. }
  454. }
  455. private func handleActivateOverride(_ presetName: String) {
  456. Task {
  457. let context = CoreDataStack.shared.newTaskContext()
  458. // Fetch all presets to find the one to activate
  459. let presetIds = await overrideStorage.fetchForOverridePresets()
  460. let presets: [OverrideStored] = await CoreDataStack.shared
  461. .getNSManagedObject(with: presetIds, context: context)
  462. // Check for active override
  463. if let activeOverrideId = await overrideStorage.fetchLatestActiveOverride() {
  464. let activeOverride = await context.perform {
  465. context.object(with: activeOverrideId) as? OverrideStored
  466. }
  467. // Deactivate if exists
  468. if let override = activeOverride {
  469. await context.perform {
  470. override.enabled = false
  471. }
  472. }
  473. }
  474. // Activate the selected preset
  475. await context.perform {
  476. if let presetToActivate = presets.first(where: { $0.name == presetName }) {
  477. presetToActivate.enabled = true
  478. presetToActivate.date = Date()
  479. do {
  480. guard context.hasChanges else { return }
  481. try context.save()
  482. debug(.watchManager, "📱 Successfully activated override: \(presetName)")
  483. // Send notification to update Adjustments UI
  484. Foundation.NotificationCenter.default.post(
  485. name: .didUpdateOverrideConfiguration,
  486. object: nil
  487. )
  488. } catch {
  489. debug(.watchManager, "❌ Error activating override: \(error.localizedDescription)")
  490. }
  491. }
  492. }
  493. }
  494. }
  495. private func handleCancelTempTarget() {
  496. Task {
  497. let context = CoreDataStack.shared.newTaskContext()
  498. if let tempTargetId = await tempTargetStorage.loadLatestTempTargetConfigurations(fetchLimit: 1).first {
  499. let tempTarget = await context.perform {
  500. context.object(with: tempTargetId) as? TempTargetStored
  501. }
  502. await context.perform {
  503. if let activeTempTarget = tempTarget {
  504. activeTempTarget.enabled = false
  505. do {
  506. guard context.hasChanges else { return }
  507. try context.save()
  508. debug(.watchManager, "📱 Successfully cancelled temp target")
  509. // To cancel the temp target also for oref
  510. self.tempTargetStorage.saveTempTargetsToStorage([TempTarget.cancel(at: Date())])
  511. // Send notification to update Adjustments UI
  512. Foundation.NotificationCenter.default.post(
  513. name: .didUpdateTempTargetConfiguration,
  514. object: nil
  515. )
  516. } catch {
  517. debug(.watchManager, "❌ Error cancelling temp target: \(error.localizedDescription)")
  518. }
  519. }
  520. }
  521. }
  522. }
  523. }
  524. private func handleActivateTempTarget(_ presetName: String) {
  525. Task {
  526. let context = CoreDataStack.shared.newTaskContext()
  527. // Fetch all presets to find the one to activate
  528. let presetIds = await tempTargetStorage.fetchForTempTargetPresets()
  529. let presets: [TempTargetStored] = await CoreDataStack.shared
  530. .getNSManagedObject(with: presetIds, context: context)
  531. // Check for active temp target
  532. if let activeTempTargetId = await tempTargetStorage.loadLatestTempTargetConfigurations(fetchLimit: 1).first {
  533. let activeTempTarget = await context.perform {
  534. context.object(with: activeTempTargetId) as? TempTargetStored
  535. }
  536. // Deactivate if exists
  537. if let tempTarget = activeTempTarget {
  538. await context.perform {
  539. tempTarget.enabled = false
  540. }
  541. }
  542. }
  543. // Activate the selected preset
  544. await context.perform {
  545. if let presetToActivate = presets.first(where: { $0.name == presetName }) {
  546. presetToActivate.enabled = true
  547. presetToActivate.date = Date()
  548. do {
  549. guard context.hasChanges else { return }
  550. try context.save()
  551. debug(.watchManager, "📱 Successfully activated temp target: \(presetName)")
  552. let settingsHalfBasalTarget = self.settingsManager.preferences
  553. .halfBasalExerciseTarget
  554. let halfBasalTarget = presetToActivate.halfBasalTarget?.decimalValue
  555. // To activate the temp target also in oref
  556. let tempTarget = TempTarget(
  557. name: presetToActivate.name,
  558. createdAt: Date(),
  559. targetTop: presetToActivate.target?.decimalValue,
  560. targetBottom: presetToActivate.target?.decimalValue,
  561. duration: presetToActivate.duration?.decimalValue ?? 0,
  562. enteredBy: TempTarget.local,
  563. reason: TempTarget.custom,
  564. isPreset: true,
  565. enabled: true,
  566. halfBasalTarget: halfBasalTarget ?? settingsHalfBasalTarget
  567. )
  568. self.tempTargetStorage.saveTempTargetsToStorage([tempTarget])
  569. // Send notification to update Adjustments UI
  570. Foundation.NotificationCenter.default.post(
  571. name: .didUpdateTempTargetConfiguration,
  572. object: nil
  573. )
  574. } catch {
  575. debug(.watchManager, "❌ Error activating temp target: \(error.localizedDescription)")
  576. }
  577. }
  578. }
  579. }
  580. }
  581. private func subscribeToBolusProgress() {
  582. apsManager.bolusProgress
  583. .receive(on: DispatchQueue.main)
  584. .sink { [weak self] progress in
  585. self?.sendBolusProgressToWatch(progress: progress)
  586. }
  587. .store(in: &subscriptions)
  588. }
  589. private func sendBolusProgressToWatch(progress: Decimal?) {
  590. guard let session = session, session.isReachable, let progress = progress else { return }
  591. let message: [String: Any] = ["bolusProgress": Double(truncating: progress as NSNumber)]
  592. session.sendMessage(message, replyHandler: nil) { error in
  593. debug(.watchManager, "❌ Error sending bolus progress: \(error.localizedDescription)")
  594. }
  595. }
  596. }
  597. // TODO: - is there a better approach than setting up the watch state every time a setting has changed?
  598. extension BaseWatchManager: SettingsObserver, PumpSettingsObserver, PreferencesObserver {
  599. // to update maxCOB, maxIOB
  600. func preferencesDidChange(_: Preferences) {
  601. Task {
  602. let state = await self.setupWatchState()
  603. self.sendDataToWatch(state)
  604. }
  605. }
  606. // to update maxBolus
  607. func pumpSettingsDidChange(_: PumpSettings) {
  608. Task {
  609. let state = await self.setupWatchState()
  610. self.sendDataToWatch(state)
  611. }
  612. }
  613. // to update the rest
  614. func settingsDidChange(_: TrioSettings) {
  615. Task {
  616. let state = await self.setupWatchState()
  617. self.sendDataToWatch(state)
  618. }
  619. }
  620. }