AppleWatchManager.swift 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699
  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 Data 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. // MARK: - WCSessionDelegate
  256. func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
  257. if let error = error {
  258. debug(.watchManager, "📱 Phone session activation failed: \(error.localizedDescription)")
  259. return
  260. }
  261. debug(.watchManager, "📱 Phone session activated with state: \(activationState.rawValue)")
  262. debug(.watchManager, "📱 Phone isReachable after activation: \(session.isReachable)")
  263. // Try to send initial data after activation
  264. Task {
  265. let state = await self.setupWatchState()
  266. self.sendDataToWatch(state)
  267. }
  268. }
  269. func session(_: WCSession, didReceiveMessage message: [String: Any]) {
  270. DispatchQueue.main.async { [weak self] in
  271. if let bolusAmount = message["bolus"] as? Double,
  272. message["carbs"] == nil,
  273. message["date"] == nil
  274. {
  275. debug(.watchManager, "📱 Received bolus request from watch: \(bolusAmount)U")
  276. self?.handleBolusRequest(Decimal(bolusAmount))
  277. } else if let carbsAmount = message["carbs"] as? Int,
  278. let timestamp = message["date"] as? TimeInterval,
  279. message["bolus"] == nil
  280. {
  281. let date = Date(timeIntervalSince1970: timestamp)
  282. debug(.watchManager, "📱 Received carbs request from watch: \(carbsAmount)g at \(date)")
  283. self?.handleCarbsRequest(carbsAmount, date)
  284. } else if let bolusAmount = message["bolus"] as? Double,
  285. let carbsAmount = message["carbs"] as? Int,
  286. let timestamp = message["date"] as? TimeInterval
  287. {
  288. let date = Date(timeIntervalSince1970: timestamp)
  289. debug(
  290. .watchManager,
  291. "📱 Received meal bolus combo request from watch: \(bolusAmount)U, \(carbsAmount)g at \(date)"
  292. )
  293. self?.handleCombinedRequest(bolusAmount: Decimal(bolusAmount), carbsAmount: Decimal(carbsAmount), date: date)
  294. } else {
  295. debug(.watchManager, "📱 Invalid or incomplete data received from watch. Received: \(message)")
  296. }
  297. if message["cancelOverride"] as? Bool == true {
  298. debug(.watchManager, "📱 Received cancel override request from watch")
  299. self?.handleCancelOverride()
  300. }
  301. if let presetName = message["activateOverride"] as? String {
  302. debug(.watchManager, "📱 Received activate override request from watch for preset: \(presetName)")
  303. self?.handleActivateOverride(presetName)
  304. }
  305. if let presetName = message["activateTempTarget"] as? String {
  306. debug(.watchManager, "📱 Received activate temp target request from watch for preset: \(presetName)")
  307. self?.handleActivateTempTarget(presetName)
  308. }
  309. if message["cancelTempTarget"] as? Bool == true {
  310. debug(.watchManager, "📱 Received cancel temp target request from watch")
  311. self?.handleCancelTempTarget()
  312. }
  313. // Handle bolus cancellation
  314. if message["cancelBolus"] as? Bool == true {
  315. Task {
  316. await self?.apsManager.cancelBolus()
  317. debug(.watchManager, "📱 Bolus cancelled from watch")
  318. }
  319. }
  320. }
  321. }
  322. #if os(iOS)
  323. func sessionDidBecomeInactive(_: WCSession) {}
  324. func sessionDidDeactivate(_ session: WCSession) {
  325. session.activate()
  326. }
  327. #endif
  328. func sessionReachabilityDidChange(_ session: WCSession) {
  329. debug(.watchManager, "📱 Phone reachability changed: \(session.isReachable)")
  330. if session.isReachable {
  331. // Try to send data when connection is established
  332. Task {
  333. let state = await self.setupWatchState()
  334. self.sendDataToWatch(state)
  335. }
  336. } else {
  337. // Try to reconnect after a short delay
  338. DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
  339. self?.retryConnection()
  340. }
  341. }
  342. }
  343. /// Processes bolus requests received from the Watch
  344. /// - Parameter amount: The requested bolus amount in units
  345. private func handleBolusRequest(_ amount: Decimal) {
  346. Task {
  347. await apsManager.enactBolus(amount: Double(amount), isSMB: false)
  348. debug(.watchManager, "📱 Enacted bolus via APS Manager: \(amount)U")
  349. }
  350. }
  351. /// Handles carbs entry requests received from the Watch
  352. /// - Parameters:
  353. /// - amount: The carbs amount in grams
  354. /// - date: Timestamp for the carbs entry
  355. private func handleCarbsRequest(_ amount: Int, _ date: Date) {
  356. Task {
  357. let context = CoreDataStack.shared.newTaskContext()
  358. await context.perform {
  359. let carbs = CarbEntryStored(context: context)
  360. carbs.carbs = Double(truncating: amount as NSNumber)
  361. carbs.date = date
  362. carbs.id = UUID()
  363. // TODO: add FPU
  364. do {
  365. guard context.hasChanges else { return }
  366. try context.save()
  367. debug(.watchManager, "📱 Saved carbs from watch: \(amount)g at \(date)")
  368. } catch {
  369. debug(.watchManager, "❌ Error saving carbs: \(error.localizedDescription)")
  370. }
  371. }
  372. }
  373. }
  374. /// Handles combined bolus and carbs entry requests received from the Watch.
  375. /// - Parameters:
  376. /// - bolusAmount: The bolus amount in units
  377. /// - carbsAmount: The carbs amount in grams
  378. /// - date: Timestamp for the carbs entry
  379. private func handleCombinedRequest(bolusAmount: Decimal, carbsAmount: Decimal, date: Date) {
  380. Task {
  381. let context = CoreDataStack.shared.newTaskContext()
  382. do {
  383. // Save carbs entry in Core Data
  384. try await context.perform {
  385. let carbEntry = CarbEntryStored(context: context)
  386. carbEntry.carbs = NSDecimalNumber(decimal: carbsAmount).doubleValue
  387. carbEntry.date = date
  388. // TODO: Add Fat-Protein Units (FPU) logic if required
  389. guard context.hasChanges else { return }
  390. try context.save()
  391. debug(.watchManager, "📱 Saved carbs from watch: \(carbsAmount)g at \(date)")
  392. }
  393. // Enact bolus via APS Manager
  394. let bolusDouble = NSDecimalNumber(decimal: bolusAmount).doubleValue
  395. await apsManager.enactBolus(amount: bolusDouble, isSMB: false)
  396. debug(.watchManager, "📱 Enacted bolus from watch via APS Manager: \(bolusDouble)U")
  397. } catch {
  398. debug(.watchManager, "❌ Error processing combined request: \(error.localizedDescription)")
  399. }
  400. }
  401. }
  402. private func handleCancelOverride() {
  403. Task {
  404. let context = CoreDataStack.shared.newTaskContext()
  405. if let overrideId = await overrideStorage.fetchLatestActiveOverride() {
  406. let override = await context.perform {
  407. context.object(with: overrideId) as? OverrideStored
  408. }
  409. await context.perform {
  410. if let activeOverride = override {
  411. activeOverride.enabled = false
  412. do {
  413. guard context.hasChanges else { return }
  414. try context.save()
  415. debug(.watchManager, "📱 Successfully cancelled override")
  416. // Send notification to update Adjustments UI
  417. Foundation.NotificationCenter.default.post(
  418. name: .didUpdateOverrideConfiguration,
  419. object: nil
  420. )
  421. } catch {
  422. debug(.watchManager, "❌ Error cancelling override: \(error.localizedDescription)")
  423. }
  424. }
  425. }
  426. }
  427. }
  428. }
  429. private func handleActivateOverride(_ presetName: String) {
  430. Task {
  431. let context = CoreDataStack.shared.newTaskContext()
  432. // Fetch all presets to find the one to activate
  433. let presetIds = await overrideStorage.fetchForOverridePresets()
  434. let presets: [OverrideStored] = await CoreDataStack.shared
  435. .getNSManagedObject(with: presetIds, context: context)
  436. // Check for active override
  437. if let activeOverrideId = await overrideStorage.fetchLatestActiveOverride() {
  438. let activeOverride = await context.perform {
  439. context.object(with: activeOverrideId) as? OverrideStored
  440. }
  441. // Deactivate if exists
  442. if let override = activeOverride {
  443. await context.perform {
  444. override.enabled = false
  445. }
  446. }
  447. }
  448. // Activate the selected preset
  449. await context.perform {
  450. if let presetToActivate = presets.first(where: { $0.name == presetName }) {
  451. presetToActivate.enabled = true
  452. presetToActivate.date = Date()
  453. do {
  454. guard context.hasChanges else { return }
  455. try context.save()
  456. debug(.watchManager, "📱 Successfully activated override: \(presetName)")
  457. // Send notification to update Adjustments UI
  458. Foundation.NotificationCenter.default.post(
  459. name: .didUpdateOverrideConfiguration,
  460. object: nil
  461. )
  462. } catch {
  463. debug(.watchManager, "❌ Error activating override: \(error.localizedDescription)")
  464. }
  465. }
  466. }
  467. }
  468. }
  469. private func handleCancelTempTarget() {
  470. Task {
  471. let context = CoreDataStack.shared.newTaskContext()
  472. if let tempTargetId = await tempTargetStorage.loadLatestTempTargetConfigurations(fetchLimit: 1).first {
  473. let tempTarget = await context.perform {
  474. context.object(with: tempTargetId) as? TempTargetStored
  475. }
  476. await context.perform {
  477. if let activeTempTarget = tempTarget {
  478. activeTempTarget.enabled = false
  479. do {
  480. guard context.hasChanges else { return }
  481. try context.save()
  482. debug(.watchManager, "📱 Successfully cancelled temp target")
  483. // To cancel the temp target also for oref
  484. self.tempTargetStorage.saveTempTargetsToStorage([TempTarget.cancel(at: Date())])
  485. // Send notification to update Adjustments UI
  486. Foundation.NotificationCenter.default.post(
  487. name: .didUpdateTempTargetConfiguration,
  488. object: nil
  489. )
  490. } catch {
  491. debug(.watchManager, "❌ Error cancelling temp target: \(error.localizedDescription)")
  492. }
  493. }
  494. }
  495. }
  496. }
  497. }
  498. private func handleActivateTempTarget(_ presetName: String) {
  499. Task {
  500. let context = CoreDataStack.shared.newTaskContext()
  501. // Fetch all presets to find the one to activate
  502. let presetIds = await tempTargetStorage.fetchForTempTargetPresets()
  503. let presets: [TempTargetStored] = await CoreDataStack.shared
  504. .getNSManagedObject(with: presetIds, context: context)
  505. // Check for active temp target
  506. if let activeTempTargetId = await tempTargetStorage.loadLatestTempTargetConfigurations(fetchLimit: 1).first {
  507. let activeTempTarget = await context.perform {
  508. context.object(with: activeTempTargetId) as? TempTargetStored
  509. }
  510. // Deactivate if exists
  511. if let tempTarget = activeTempTarget {
  512. await context.perform {
  513. tempTarget.enabled = false
  514. }
  515. }
  516. }
  517. // Activate the selected preset
  518. await context.perform {
  519. if let presetToActivate = presets.first(where: { $0.name == presetName }) {
  520. presetToActivate.enabled = true
  521. presetToActivate.date = Date()
  522. do {
  523. guard context.hasChanges else { return }
  524. try context.save()
  525. debug(.watchManager, "📱 Successfully activated temp target: \(presetName)")
  526. let settingsHalfBasalTarget = self.settingsManager.preferences
  527. .halfBasalExerciseTarget
  528. let halfBasalTarget = presetToActivate.halfBasalTarget?.decimalValue
  529. // To activate the temp target also in oref
  530. let tempTarget = TempTarget(
  531. name: presetToActivate.name,
  532. createdAt: Date(),
  533. targetTop: presetToActivate.target?.decimalValue,
  534. targetBottom: presetToActivate.target?.decimalValue,
  535. duration: presetToActivate.duration?.decimalValue ?? 0,
  536. enteredBy: TempTarget.local,
  537. reason: TempTarget.custom,
  538. isPreset: true,
  539. enabled: true,
  540. halfBasalTarget: halfBasalTarget ?? settingsHalfBasalTarget
  541. )
  542. self.tempTargetStorage.saveTempTargetsToStorage([tempTarget])
  543. // Send notification to update Adjustments UI
  544. Foundation.NotificationCenter.default.post(
  545. name: .didUpdateTempTargetConfiguration,
  546. object: nil
  547. )
  548. } catch {
  549. debug(.watchManager, "❌ Error activating temp target: \(error.localizedDescription)")
  550. }
  551. }
  552. }
  553. }
  554. }
  555. private func subscribeToBolusProgress() {
  556. apsManager.bolusProgress
  557. .receive(on: DispatchQueue.main)
  558. .sink { [weak self] progress in
  559. self?.sendBolusProgressToWatch(progress: progress)
  560. }
  561. .store(in: &subscriptions)
  562. }
  563. private func sendBolusProgressToWatch(progress: Decimal?) {
  564. guard let session = session, session.isReachable, let progress = progress else { return }
  565. let message: [String: Any] = ["bolusProgress": Double(truncating: progress as NSNumber)]
  566. session.sendMessage(message, replyHandler: nil) { error in
  567. debug(.watchManager, "❌ Error sending bolus progress: \(error.localizedDescription)")
  568. }
  569. }
  570. }
  571. // TODO: - is there a better approach than setting up the watch state every time a setting has changed?
  572. extension BaseWatchManager: SettingsObserver, PumpSettingsObserver, PreferencesObserver {
  573. // to update maxCOB, maxIOB
  574. func preferencesDidChange(_: Preferences) {
  575. Task {
  576. let state = await self.setupWatchState()
  577. self.sendDataToWatch(state)
  578. }
  579. }
  580. // to update maxBolus
  581. func pumpSettingsDidChange(_: PumpSettings) {
  582. Task {
  583. let state = await self.setupWatchState()
  584. self.sendDataToWatch(state)
  585. }
  586. }
  587. // to update the rest
  588. func settingsDidChange(_: TrioSettings) {
  589. Task {
  590. let state = await self.setupWatchState()
  591. self.sendDataToWatch(state)
  592. }
  593. }
  594. }