AppleWatchManager.swift 56 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270
  1. import Combine
  2. import CoreData
  3. import Foundation
  4. import Swinject
  5. import UIKit
  6. import WatchConnectivity
  7. /// Protocol defining the base functionality for Watch communication
  8. protocol WatchManager {
  9. func setupWatchState() async -> WatchState
  10. }
  11. /// Main implementation of the Watch communication manager
  12. /// Handles bidirectional communication between iPhone and Apple Watch
  13. final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchManager {
  14. private var session: WCSession?
  15. @Injected() var broadcaster: Broadcaster!
  16. @Injected() private var apsManager: APSManager!
  17. @Injected() private var settingsManager: SettingsManager!
  18. @Injected() private var fileStorage: FileStorage!
  19. @Injected() private var glucoseStorage: GlucoseStorage!
  20. @Injected() private var determinationStorage: DeterminationStorage!
  21. @Injected() private var overrideStorage: OverrideStorage!
  22. @Injected() private var tempTargetStorage: TempTargetsStorage!
  23. @Injected() private var bolusCalculationManager: BolusCalculationManager!
  24. @Injected() private var iobService: IOBService!
  25. private var units: GlucoseUnits = .mgdL
  26. private var glucoseColorScheme: GlucoseColorScheme = .staticColor
  27. private var lowGlucose: Decimal = 70.0
  28. private var highGlucose: Decimal = 180.0
  29. private var currentGlucoseTarget: Decimal = 100.0
  30. private var activeBolusAmount: Double = 0.0
  31. // Queue for handling Core Data change notifications
  32. private let queue = DispatchQueue(label: "BaseWatchManagerManager.queue", qos: .utility)
  33. private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
  34. private var subscriptions = Set<AnyCancellable>()
  35. typealias PumpEvent = PumpEventStored.EventType
  36. let backgroundContext = CoreDataStack.shared.newTaskContext()
  37. let viewContext = CoreDataStack.shared.persistentContainer.viewContext
  38. init(resolver: Resolver) {
  39. super.init()
  40. injectServices(resolver)
  41. setupWatchSession()
  42. units = settingsManager.settings.units
  43. glucoseColorScheme = settingsManager.settings.glucoseColorScheme
  44. lowGlucose = settingsManager.settings.low
  45. highGlucose = settingsManager.settings.high
  46. Task {
  47. currentGlucoseTarget = await getCurrentGlucoseTarget() ?? Decimal(100)
  48. }
  49. broadcaster.register(SettingsObserver.self, observer: self)
  50. broadcaster.register(PumpSettingsObserver.self, observer: self)
  51. // Observer for OrefDetermination and adjustments
  52. coreDataPublisher =
  53. changedObjectsOnManagedObjectContextDidSavePublisher()
  54. .receive(on: queue)
  55. .share()
  56. .eraseToAnyPublisher()
  57. // Observer for glucose and manual glucose
  58. glucoseStorage.updatePublisher
  59. .receive(on: DispatchQueue.global(qos: .background))
  60. .sink { [weak self] _ in
  61. guard let self = self else { return }
  62. // Skip if no watch is paired or app not installed
  63. guard let session = self.session, session.isPaired, session.isReachable,
  64. session.isWatchAppInstalled else { return }
  65. Task {
  66. let state = await self.setupWatchState()
  67. await self.sendDataToWatch(state)
  68. }
  69. }
  70. .store(in: &subscriptions)
  71. iobService.iobPublisher
  72. .receive(on: DispatchQueue.global(qos: .background))
  73. .sink { [weak self] _ in
  74. guard let self = self else { return }
  75. Task {
  76. let state = await self.setupWatchState()
  77. await self.sendDataToWatch(state)
  78. }
  79. }
  80. .store(in: &subscriptions)
  81. registerHandlers()
  82. }
  83. private func registerHandlers() {
  84. coreDataPublisher?.filteredByEntityName("OrefDetermination").sink { [weak self] _ in
  85. guard let self = self else { return }
  86. // Skip if no watch is paired or app not installed
  87. guard let session = self.session, session.isPaired, session.isReachable, session.isWatchAppInstalled else { return }
  88. Task {
  89. let state = await self.setupWatchState()
  90. await self.sendDataToWatch(state)
  91. }
  92. }.store(in: &subscriptions)
  93. // Due to the Batch insert this only is used for observing Deletion of Glucose entries
  94. coreDataPublisher?.filteredByEntityName("GlucoseStored").sink { [weak self] _ in
  95. guard let self = self else { return }
  96. // Skip if no watch is paired or app not installed
  97. guard let session = self.session, session.isPaired, session.isReachable, session.isWatchAppInstalled else { return }
  98. Task {
  99. let state = await self.setupWatchState()
  100. await self.sendDataToWatch(state)
  101. }
  102. }.store(in: &subscriptions)
  103. coreDataPublisher?.filteredByEntityName("PumpEventStored").sink { [weak self] _ in
  104. guard let self = self else { return }
  105. Task {
  106. await self.getActiveBolusAmount()
  107. }
  108. }.store(in: &subscriptions)
  109. coreDataPublisher?.filteredByEntityName("OverrideStored").sink { [weak self] _ in
  110. guard let self = self else { return }
  111. // Skip if no watch is paired or app not installed
  112. guard let session = self.session, session.isPaired, session.isReachable, session.isWatchAppInstalled else { return }
  113. Task {
  114. let state = await self.setupWatchState()
  115. await self.sendDataToWatch(state)
  116. }
  117. }.store(in: &subscriptions)
  118. coreDataPublisher?.filteredByEntityName("TempTargetStored").sink { [weak self] _ in
  119. guard let self = self else { return }
  120. // Skip if no watch is paired or app not installed
  121. guard let session = self.session, session.isPaired, session.isReachable, session.isWatchAppInstalled else { return }
  122. Task {
  123. let state = await self.setupWatchState()
  124. await self.sendDataToWatch(state)
  125. }
  126. }.store(in: &subscriptions)
  127. }
  128. /// Sets up the WatchConnectivity session if the device supports it
  129. private func setupWatchSession() {
  130. if WCSession.isSupported() {
  131. let session = WCSession.default
  132. session.delegate = self
  133. session.activate()
  134. self.session = session
  135. debug(.watchManager, "📱 Phone session setup - isPaired: \(session.isPaired)")
  136. } else {
  137. debug(.watchManager, "📱 WCSession is not supported on this device")
  138. }
  139. }
  140. /// Attempts to reestablish the Watch connection if it becomes unreachable
  141. private func retryConnection() {
  142. guard let session = session else { return }
  143. if !session.isReachable {
  144. debug(.watchManager, "📱 Attempting to reactivate session...")
  145. session.activate()
  146. }
  147. }
  148. /// Prepares the current state data to be sent to the Watch
  149. /// - Returns: WatchState containing current glucose readings and trends and determination infos for displaying cob and iob in the view
  150. func setupWatchState() async -> WatchState {
  151. // Check if a watch is paired and reachable before doing expensive calculations
  152. guard let session = session, session.isPaired, session.isReachable, session.isWatchAppInstalled else {
  153. debug(.watchManager, "⌚️❌ Skipping setupWatchState - No Watch is paired or app not installed")
  154. return WatchState(date: Date())
  155. }
  156. // Skip if watch session is not activated
  157. guard session.activationState == .activated else {
  158. debug(.watchManager, "⌚️❌ Skipping setupWatchState - Watch session not activated")
  159. return WatchState(date: Date())
  160. }
  161. do {
  162. // Get NSManagedObjectIDs
  163. let glucoseIds = try await fetchGlucose()
  164. let determinationIds = try await determinationStorage.fetchLastDeterminationObjectID(
  165. predicate: NSPredicate.predicateFor30MinAgoForDetermination
  166. )
  167. let overridePresetIds = try await overrideStorage.fetchForOverridePresets()
  168. let tempTargetPresetIds = try await tempTargetStorage.fetchForTempTargetPresets()
  169. // Get NSManagedObjects
  170. let glucoseObjects: [GlucoseStored] = try await CoreDataStack.shared
  171. .getNSManagedObject(with: glucoseIds, context: backgroundContext)
  172. let determinationObjects: [OrefDetermination] = try await CoreDataStack.shared
  173. .getNSManagedObject(with: determinationIds, context: backgroundContext)
  174. let overridePresetObjects: [OverrideStored] = try await CoreDataStack.shared
  175. .getNSManagedObject(with: overridePresetIds, context: backgroundContext)
  176. let tempTargetPresetObjects: [TempTargetStored] = try await CoreDataStack.shared
  177. .getNSManagedObject(with: tempTargetPresetIds, context: backgroundContext)
  178. return await backgroundContext.perform {
  179. var watchState = WatchState(date: Date())
  180. // Set lastLoopDate
  181. let lastLoopMinutes = Int((Date().timeIntervalSince(self.apsManager.lastLoopDate) - 30) / 60) + 1
  182. if lastLoopMinutes > 1440 {
  183. watchState.lastLoopTime = "--"
  184. } else {
  185. watchState.lastLoopTime = "\(lastLoopMinutes) min"
  186. }
  187. // Set IOB and COB from latest determination
  188. let iob = self.iobService.currentIOB ?? 0
  189. watchState.iob = Formatter.decimalFormatterWithTwoFractionDigits.string(from: iob as NSNumber)
  190. if let latestDetermination = determinationObjects.first {
  191. let cob = NSNumber(value: latestDetermination.cob)
  192. watchState.cob = Formatter.integerFormatter.string(from: cob)
  193. }
  194. // Set override presets with their enabled status
  195. watchState.overridePresets = overridePresetObjects.map { override in
  196. OverridePresetWatch(
  197. name: override.name ?? "",
  198. isEnabled: override.enabled
  199. )
  200. }
  201. guard let latestGlucose = glucoseObjects.first else {
  202. return watchState
  203. }
  204. // Assign currentGlucose and its color
  205. /// Set current glucose with proper formatting
  206. if self.units == .mgdL {
  207. watchState.currentGlucose = "\(latestGlucose.glucose)"
  208. } else {
  209. let mgdlValue = Decimal(latestGlucose.glucose)
  210. let latestGlucoseValue = mgdlValue.formattedAsMmolL
  211. watchState.currentGlucose = "\(latestGlucoseValue)"
  212. }
  213. /// Calculate latest color
  214. let hardCodedLow = Decimal(55)
  215. let hardCodedHigh = Decimal(220)
  216. let isDynamicColorScheme = self.glucoseColorScheme == .dynamicColor
  217. let highGlucoseValue = isDynamicColorScheme ? hardCodedHigh : self.highGlucose
  218. let lowGlucoseValue = isDynamicColorScheme ? hardCodedLow : self.lowGlucose
  219. let highGlucoseColorValue = highGlucoseValue
  220. let lowGlucoseColorValue = lowGlucoseValue
  221. let targetGlucose = self.currentGlucoseTarget
  222. let currentGlucoseColor = Trio.getDynamicGlucoseColor(
  223. glucoseValue: Decimal(latestGlucose.glucose),
  224. highGlucoseColorValue: highGlucoseColorValue,
  225. lowGlucoseColorValue: lowGlucoseColorValue,
  226. targetGlucose: targetGlucose,
  227. glucoseColorScheme: self.glucoseColorScheme
  228. )
  229. if Decimal(latestGlucose.glucose) <= self.lowGlucose || Decimal(latestGlucose.glucose) >= self.highGlucose {
  230. watchState.currentGlucoseColorString = currentGlucoseColor.toHexString()
  231. } else {
  232. watchState.currentGlucoseColorString = "#ffffff" // white when in range; colored when out of range
  233. }
  234. // Map glucose values
  235. watchState.glucoseValues = glucoseObjects.compactMap { glucose in
  236. let glucoseValue = self.units == .mgdL
  237. ? Double(glucose.glucose)
  238. : Double(truncating: Decimal(glucose.glucose).asMmolL as NSNumber)
  239. let glucoseColor = Trio.getDynamicGlucoseColor(
  240. glucoseValue: Decimal(glucose.glucose),
  241. highGlucoseColorValue: highGlucoseColorValue,
  242. lowGlucoseColorValue: lowGlucoseColorValue,
  243. targetGlucose: targetGlucose,
  244. glucoseColorScheme: self.glucoseColorScheme
  245. )
  246. return WatchGlucoseObject(
  247. date: glucose.date ?? Date(),
  248. glucose: glucoseValue,
  249. color: glucoseColor.toHexString()
  250. )
  251. }
  252. .sorted { $0.date < $1.date }
  253. // Set axis domain: min and max Y-axis values
  254. // Apply unit parsing conditionally, if user uses mmol/L
  255. let maxGlucoseValue = Decimal(glucoseObjects.map { Int($0.glucose) }.max() ?? 200)
  256. var maxYValue = Decimal(200)
  257. if maxGlucoseValue > maxYValue, maxGlucoseValue <= 225 {
  258. maxYValue = Decimal(250)
  259. } else if maxGlucoseValue > 225, maxGlucoseValue <= 275 {
  260. maxYValue = Decimal(300)
  261. } else if maxGlucoseValue > 275, maxGlucoseValue <= 325 {
  262. maxYValue = Decimal(350)
  263. } else if maxGlucoseValue > 325 {
  264. maxYValue = Decimal(400)
  265. }
  266. if self.units == .mmolL {
  267. maxYValue = Double(truncating: maxYValue as NSNumber).asMmolL
  268. }
  269. watchState.maxYAxisValue = maxYValue
  270. if self.units == .mmolL {
  271. let minYValue = Double(truncating: watchState.minYAxisValue as NSNumber).asMmolL
  272. watchState.minYAxisValue = minYValue
  273. }
  274. // Convert direction to trend string
  275. watchState.trend = latestGlucose.direction
  276. // Calculate delta if we have at least 2 readings
  277. if glucoseObjects.count >= 2 {
  278. var glucoseLast = Decimal(glucoseObjects[0].glucose)
  279. var glucoseSecondLast = Decimal(glucoseObjects[1].glucose)
  280. if self.units == .mmolL {
  281. glucoseLast = glucoseLast.asMmolL
  282. glucoseSecondLast = glucoseSecondLast.asMmolL
  283. }
  284. let deltaValue = glucoseLast - glucoseSecondLast
  285. let formattedDelta = Formatter.glucoseFormatter(for: self.units)
  286. .string(from: deltaValue as NSNumber) ?? "0"
  287. watchState.delta = deltaValue < 0 ? "\(formattedDelta)" : "+\(formattedDelta)"
  288. }
  289. // Set temp target presets with their enabled status
  290. watchState.tempTargetPresets = tempTargetPresetObjects.map { tempTarget in
  291. TempTargetPresetWatch(
  292. name: tempTarget.name ?? "",
  293. isEnabled: tempTarget.enabled
  294. )
  295. }
  296. // Set units
  297. watchState.units = self.units
  298. // Add limits and pump specific dosing increment settings values
  299. watchState.maxBolus = self.settingsManager.pumpSettings.maxBolus
  300. watchState.maxCarbs = self.settingsManager.settings.maxCarbs
  301. watchState.maxFat = self.settingsManager.settings.maxFat
  302. watchState.maxProtein = self.settingsManager.settings.maxProtein
  303. watchState.bolusIncrement = self.settingsManager.preferences.bolusIncrement
  304. watchState.confirmBolusFaster = self.settingsManager.settings.confirmBolusFaster
  305. debug(
  306. .watchManager,
  307. "📱 Setup WatchState - currentGlucose: \(watchState.currentGlucose ?? "nil"), trend: \(watchState.trend ?? "nil"), delta: \(watchState.delta ?? "nil"), values: \(watchState.glucoseValues.count)"
  308. )
  309. return watchState
  310. }
  311. } catch {
  312. debug(
  313. .watchManager,
  314. "\(DebuggingIdentifiers.failed) Error setting up watch state: \(error)"
  315. )
  316. // Return empty state in case of error
  317. return WatchState(date: Date())
  318. }
  319. }
  320. /// Fetches recent glucose readings from CoreData
  321. /// - Returns: Array of NSManagedObjectIDs for glucose readings
  322. private func fetchGlucose() async throws -> [NSManagedObjectID] {
  323. let results = try await CoreDataStack.shared.fetchEntitiesAsync(
  324. ofType: GlucoseStored.self,
  325. onContext: backgroundContext,
  326. predicate: NSPredicate.glucose,
  327. key: "date",
  328. ascending: false,
  329. fetchLimit: 288
  330. )
  331. return try await backgroundContext.perform {
  332. guard let fetchedResults = results as? [GlucoseStored] else {
  333. throw CoreDataError.fetchError(function: #function, file: #file)
  334. }
  335. return fetchedResults.map(\.objectID)
  336. }
  337. }
  338. /// Fetches last pump event that is a non-external bolus from CoreData
  339. /// - Returns: NSManagedObjectIDs for last bolus
  340. func fetchLastBolus() async throws -> NSManagedObjectID? {
  341. let results = try await CoreDataStack.shared.fetchEntitiesAsync(
  342. ofType: PumpEventStored.self,
  343. onContext: backgroundContext,
  344. predicate: NSPredicate.lastPumpBolus,
  345. key: "timestamp",
  346. ascending: false,
  347. fetchLimit: 1
  348. )
  349. return try await backgroundContext.perform {
  350. guard let fetchedResults = results as? [PumpEventStored] else {
  351. throw CoreDataError.fetchError(function: #function, file: #file)
  352. }
  353. return fetchedResults.map(\.objectID).first
  354. }
  355. }
  356. /// Gets the active bolus amount by fetching last (active) bolus.
  357. @MainActor func getActiveBolusAmount() async {
  358. do {
  359. if let lastBolusObjectId = try await fetchLastBolus() {
  360. let lastBolusObject: [PumpEventStored] = try await CoreDataStack.shared
  361. .getNSManagedObject(with: [lastBolusObjectId], context: viewContext)
  362. activeBolusAmount = lastBolusObject.first?.bolus?.amount?.doubleValue ?? 0.0
  363. }
  364. } catch {
  365. debug(
  366. .default,
  367. "\(DebuggingIdentifiers.failed) Error getting active bolus amount: \(error)"
  368. )
  369. }
  370. }
  371. // MARK: - Send to Watch
  372. func watchStateToDictionary(from state: WatchState) -> [String: Any] {
  373. [
  374. WatchMessageKeys.date: state.date.timeIntervalSince1970,
  375. WatchMessageKeys.currentGlucose: state.currentGlucose ?? "--",
  376. WatchMessageKeys.currentGlucoseColorString: state.currentGlucoseColorString ?? "#ffffff",
  377. WatchMessageKeys.trend: state.trend ?? "",
  378. WatchMessageKeys.delta: state.delta ?? "",
  379. WatchMessageKeys.iob: state.iob ?? "",
  380. WatchMessageKeys.cob: state.cob ?? "",
  381. WatchMessageKeys.lastLoopTime: state.lastLoopTime ?? "",
  382. WatchMessageKeys.glucoseValues: state.glucoseValues.map { value in
  383. [
  384. "glucose": value.glucose,
  385. "date": value.date.timeIntervalSince1970,
  386. "color": value.color
  387. ]
  388. },
  389. WatchMessageKeys.minYAxisValue: state.minYAxisValue,
  390. WatchMessageKeys.maxYAxisValue: state.maxYAxisValue,
  391. WatchMessageKeys.overridePresets: state.overridePresets.map { preset in
  392. [
  393. "name": preset.name,
  394. "isEnabled": preset.isEnabled
  395. ]
  396. },
  397. WatchMessageKeys.tempTargetPresets: state.tempTargetPresets.map { preset in
  398. [
  399. "name": preset.name,
  400. "isEnabled": preset.isEnabled
  401. ]
  402. },
  403. WatchMessageKeys.maxBolus: state.maxBolus,
  404. WatchMessageKeys.maxCarbs: state.maxCarbs,
  405. WatchMessageKeys.maxFat: state.maxFat,
  406. WatchMessageKeys.maxProtein: state.maxProtein,
  407. WatchMessageKeys.bolusIncrement: state.bolusIncrement,
  408. WatchMessageKeys.confirmBolusFaster: state.confirmBolusFaster,
  409. WatchMessageKeys.units: state.units.rawValue
  410. ]
  411. }
  412. /// Sends the state of type WatchState to the connected Watch
  413. /// - Parameter state: Current WatchState containing glucose data to be sent
  414. @MainActor func sendDataToWatch(_ state: WatchState) async {
  415. guard let session = session else { return }
  416. guard session.isPaired else {
  417. debug(.watchManager, "⌚️❌ No Watch is paired")
  418. return
  419. }
  420. guard session.isWatchAppInstalled else {
  421. debug(.watchManager, "⌚️❌ Trio Watch app is")
  422. return
  423. }
  424. guard session.activationState == .activated else {
  425. let activationStateString = "\(session.activationState)"
  426. debug(.watchManager, "⌚️ Watch session activationState = \(activationStateString). Reactivating...")
  427. session.activate()
  428. return
  429. }
  430. // Skip if we already sent this state or older
  431. let lastSent = WatchStateSnapshot.loadLatestDateFromDisk()
  432. guard lastSent < state.date else {
  433. debug(.watchManager, "🕐 Skipping push — newer or equal state already sent")
  434. return
  435. }
  436. let message: [String: Any] = watchStateToDictionary(from: state)
  437. // if session is reachable, it means watch App is in the foreground -> send watchState as message
  438. // if session is not reachable, it means it's in background -> send watchState as userInfo
  439. if session.isReachable {
  440. session.sendMessage([WatchMessageKeys.watchState: message], replyHandler: nil) { error in
  441. debug(.watchManager, "❌ Error sending watch state: \(error)")
  442. }
  443. WatchStateSnapshot.saveLatestDateToDisk(state.date)
  444. } else {
  445. WatchStateSnapshot.saveLatestDateToDisk(state.date)
  446. session.transferUserInfo([WatchMessageKeys.watchState: message])
  447. debug(.watchManager, "📤 Transferred new WatchState snapshot via userInfo")
  448. }
  449. }
  450. func sendAcknowledgment(toWatch success: Bool, message: String = "", ackCode: AcknowledgmentCode) {
  451. guard let session = session, session.isReachable else {
  452. debug(.watchManager, "⌚️ Watch not reachable for acknowledgment")
  453. return
  454. }
  455. let ackMessage: [String: Any] = [
  456. WatchMessageKeys.acknowledged: success,
  457. WatchMessageKeys.message: message,
  458. WatchMessageKeys.ackCode: ackCode.rawValue
  459. ]
  460. session.sendMessage(ackMessage, replyHandler: nil) { error in
  461. debug(.watchManager, "❌ Error sending acknowledgment: \(error)")
  462. }
  463. }
  464. // MARK: - WCSessionDelegate
  465. func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
  466. if let error = error {
  467. debug(.watchManager, "📱 Phone session activation failed: \(error)")
  468. return
  469. }
  470. debug(.watchManager, "📱 Phone session activated with state: \(activationState.rawValue)")
  471. debug(.watchManager, "📱 Phone isReachable after activation: \(session.isReachable)")
  472. // Try to send initial data after activation
  473. Task {
  474. let state = await self.setupWatchState()
  475. await self.sendDataToWatch(state)
  476. }
  477. }
  478. func session(_: WCSession, didReceiveMessage message: [String: Any]) {
  479. DispatchQueue.main.async { [weak self] in
  480. if let logs = message["watchLogs"] as? String {
  481. SimpleLogReporter.appendToWatchLog(logs)
  482. }
  483. if let requestWatchUpdate = message[WatchMessageKeys.requestWatchUpdate] as? String,
  484. requestWatchUpdate == WatchMessageKeys.watchState
  485. {
  486. debug(.watchManager, "📱 Watch requested watch state data update.")
  487. guard let self = self else { return }
  488. // Skip if no watch is paired or app not installed
  489. guard let session = self.session, session.isPaired, session.isReachable,
  490. session.isWatchAppInstalled else { return }
  491. Task {
  492. let state = await self.setupWatchState()
  493. await self.sendDataToWatch(state)
  494. }
  495. return
  496. }
  497. if let bolusAmount = message[WatchMessageKeys.bolus] as? Double,
  498. message[WatchMessageKeys.carbs] == nil,
  499. message[WatchMessageKeys.date] == nil
  500. {
  501. debug(.watchManager, "📱 Received bolus request from watch: \(bolusAmount)U")
  502. self?.handleBolusRequest(Decimal(bolusAmount))
  503. } else if let carbsAmount = message[WatchMessageKeys.carbs] as? Int,
  504. let timestamp = message[WatchMessageKeys.date] as? TimeInterval,
  505. message[WatchMessageKeys.bolus] == nil
  506. {
  507. let date = Date(timeIntervalSince1970: timestamp)
  508. debug(.watchManager, "📱 Received carbs request from watch: \(carbsAmount)g at \(date)")
  509. self?.handleCarbsRequest(carbsAmount, date)
  510. } else if let bolusAmount = message[WatchMessageKeys.bolus] as? Double,
  511. let carbsAmount = message[WatchMessageKeys.carbs] as? Int,
  512. let timestamp = message[WatchMessageKeys.date] as? TimeInterval
  513. {
  514. let date = Date(timeIntervalSince1970: timestamp)
  515. debug(
  516. .watchManager,
  517. "📱 Received meal bolus combo request from watch: \(bolusAmount)U, \(carbsAmount)g at \(date)"
  518. )
  519. self?.handleCombinedRequest(bolusAmount: Decimal(bolusAmount), carbsAmount: Decimal(carbsAmount), date: date)
  520. } else {
  521. debug(.watchManager, "📱 Invalid or incomplete data received from watch. Received: \(message)")
  522. // Acknowledge failure
  523. self?.sendAcknowledgment(
  524. toWatch: false,
  525. message: "Error! Invalid or incomplete data received from watch.",
  526. ackCode: .genericFailure
  527. )
  528. }
  529. if message[WatchMessageKeys.cancelOverride] as? Bool == true {
  530. debug(.watchManager, "📱 Received cancel override request from watch")
  531. self?.handleCancelOverride()
  532. }
  533. if let presetName = message[WatchMessageKeys.activateOverride] as? String {
  534. debug(.watchManager, "📱 Received activate override request from watch for preset: \(presetName)")
  535. self?.handleActivateOverride(presetName)
  536. }
  537. if let presetName = message[WatchMessageKeys.activateTempTarget] as? String {
  538. debug(.watchManager, "📱 Received activate temp target request from watch for preset: \(presetName)")
  539. self?.handleActivateTempTarget(presetName)
  540. }
  541. if message[WatchMessageKeys.cancelTempTarget] as? Bool == true {
  542. debug(.watchManager, "📱 Received cancel temp target request from watch")
  543. self?.handleCancelTempTarget()
  544. }
  545. if message[WatchMessageKeys.requestBolusRecommendation] as? Bool == true {
  546. let carbs = message[WatchMessageKeys.carbs] as? Int ?? 0
  547. var minPredBG: Decimal = 54
  548. Task { [weak self] in
  549. guard let self = self else { return }
  550. do {
  551. // Fetch determination data
  552. let determinationIds = try await determinationStorage.fetchLastDeterminationObjectID(
  553. predicate: NSPredicate.predicateFor30MinAgoForDetermination
  554. )
  555. let determinationObjects: [OrefDetermination] = try await CoreDataStack.shared.getNSManagedObject(
  556. with: determinationIds,
  557. context: backgroundContext
  558. )
  559. await MainActor.run {
  560. minPredBG = determinationObjects.first?.minPredBGFromReason ?? 54
  561. }
  562. } catch let error as CoreDataError {
  563. debug(.default, "Core Data error: \(error)")
  564. } catch {
  565. debug(.default, "Unexpected error: \(error)")
  566. }
  567. // Get recommendation from BolusCalculationManager
  568. let result = await bolusCalculationManager.handleBolusCalculation(
  569. carbs: Decimal(carbs),
  570. useFattyMealCorrection: false,
  571. useSuperBolus: false,
  572. lastLoopDate: apsManager.lastLoopDate,
  573. minPredBG: minPredBG,
  574. simulatedCOB: nil,
  575. isBackdated: false // we cannot backdate carbs via watch
  576. )
  577. // Send recommendation back to watch
  578. let recommendationMessage: [String: Any] = [
  579. WatchMessageKeys.recommendedBolus: NSDecimalNumber(decimal: result.insulinCalculated)
  580. ]
  581. if let session = self.session, session.isReachable {
  582. debug(.watchManager, "📱 Sending recommendedBolus: \(result.insulinCalculated)")
  583. session.sendMessage(recommendationMessage, replyHandler: nil)
  584. }
  585. }
  586. return
  587. }
  588. }
  589. }
  590. func session(_: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) {
  591. if let logs = userInfo["watchLogs"] as? String {
  592. SimpleLogReporter.appendToWatchLog(logs)
  593. }
  594. }
  595. #if os(iOS)
  596. func sessionDidBecomeInactive(_: WCSession) {}
  597. func sessionDidDeactivate(_ session: WCSession) {
  598. session.activate()
  599. }
  600. #endif
  601. func sessionReachabilityDidChange(_ session: WCSession) {
  602. debug(.watchManager, "📱 Phone reachability changed: \(session.isReachable)")
  603. if session.isReachable {
  604. // Try to send data when connection is established
  605. Task {
  606. let state = await self.setupWatchState()
  607. await self.sendDataToWatch(state)
  608. }
  609. } else {
  610. // Try to reconnect after a short delay
  611. DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
  612. self?.retryConnection()
  613. }
  614. }
  615. }
  616. /// Processes bolus requests received from the Watch
  617. /// - Parameter amount: The requested bolus amount in units
  618. private func handleBolusRequest(_ amount: Decimal) {
  619. Task {
  620. await apsManager.enactBolus(amount: Double(amount), isSMB: false) { success, message in
  621. // Acknowledge success or error of bolus
  622. self.sendAcknowledgment(
  623. toWatch: success,
  624. message: message,
  625. ackCode: success == true ? .genericSuccess : .genericFailure
  626. )
  627. }
  628. debug(.watchManager, "📱 Enacted bolus via APS Manager: \(amount)U")
  629. }
  630. }
  631. /// Handles carbs entry requests received from the Watch
  632. /// - Parameters:
  633. /// - amount: The carbs amount in grams
  634. /// - date: Timestamp for the carbs entry
  635. private func handleCarbsRequest(_ amount: Int, _ date: Date) {
  636. Task {
  637. let context = CoreDataStack.shared.newTaskContext()
  638. await context.perform {
  639. let carbEntry = CarbEntryStored(context: context)
  640. carbEntry.id = UUID()
  641. carbEntry.carbs = Double(truncating: amount as NSNumber)
  642. carbEntry.date = date
  643. carbEntry.note = String(localized: "Via Watch", comment: "Note added to carb entry when entered via watch")
  644. carbEntry.isFPU = false // set this to false to ensure watch-entered carbs are displayed in main chart
  645. carbEntry.isUploadedToNS = false
  646. carbEntry.isUploadedToHealth = false
  647. carbEntry.isUploadedToTidepool = false
  648. do {
  649. guard context.hasChanges else {
  650. // Acknowledge failure
  651. self.sendAcknowledgment(
  652. toWatch: false,
  653. message: "Error! Something went wrong when processing your request.",
  654. ackCode: .genericFailure
  655. )
  656. return
  657. }
  658. try context.save()
  659. debug(.watchManager, "📱 Saved carbs from watch: \(amount)g at \(date)")
  660. // Acknowledge success
  661. self.sendAcknowledgment(
  662. toWatch: true,
  663. message: String(
  664. localized: "Carbs logged successfully.",
  665. comment: "Success message sent to watch when carbs are logged successfully"
  666. ),
  667. ackCode: .carbsLogged
  668. )
  669. } catch {
  670. debug(.watchManager, "❌ Error saving carbs: \(error)")
  671. // Acknowledge failure
  672. self.sendAcknowledgment(toWatch: false, message: "Error logging carbs", ackCode: .genericFailure)
  673. }
  674. }
  675. }
  676. }
  677. /// Handles combined bolus and carbs entry requests received from the Watch.
  678. /// - Parameters:
  679. /// - bolusAmount: The bolus amount in units
  680. /// - carbsAmount: The carbs amount in grams
  681. /// - date: Timestamp for the carbs entry
  682. private func handleCombinedRequest(bolusAmount: Decimal, carbsAmount: Decimal, date: Date) {
  683. Task {
  684. let context = CoreDataStack.shared.newTaskContext()
  685. do {
  686. // Notify Watch: "Saving carbs..."
  687. self.sendAcknowledgment(
  688. toWatch: true,
  689. message: String(
  690. localized: "Saving Carbs...",
  691. comment: "Successful message sent to watch when saving carbs"
  692. ),
  693. ackCode: .savingCarbs
  694. )
  695. // Save carbs entry in Core Data
  696. try await context.perform {
  697. let carbEntry = CarbEntryStored(context: context)
  698. carbEntry.id = UUID()
  699. carbEntry.carbs = NSDecimalNumber(decimal: carbsAmount).doubleValue
  700. carbEntry.date = date
  701. carbEntry.note = String(localized: "Via Watch", comment: "Note added to carb entry when entered via watch")
  702. carbEntry.isFPU = false // set this to false to ensure watch-entered carbs are displayed in main chart
  703. carbEntry.isUploadedToNS = false
  704. carbEntry.isUploadedToHealth = false
  705. carbEntry.isUploadedToTidepool = false
  706. guard context.hasChanges else {
  707. // Acknowledge failure
  708. self.sendAcknowledgment(
  709. toWatch: false,
  710. message: "Error! Something went wrong when processing your request.",
  711. ackCode: .genericFailure
  712. )
  713. return
  714. }
  715. try context.save()
  716. debug(.watchManager, "📱 Saved carbs from watch: \(carbsAmount) g at \(date)")
  717. }
  718. // Notify Watch: "Enacting bolus..."
  719. sendAcknowledgment(
  720. toWatch: true,
  721. message: String(
  722. localized: "Enacting bolus...",
  723. comment: "Successful message sent to watch when enacting bolus"
  724. ),
  725. ackCode: .enactingBolus
  726. )
  727. // Enact bolus via APS Manager
  728. let bolusDouble = NSDecimalNumber(decimal: bolusAmount).doubleValue
  729. await apsManager.enactBolus(amount: bolusDouble, isSMB: false) { success, message in
  730. // Acknowledge success or error of bolus
  731. self.sendAcknowledgment(
  732. toWatch: success,
  733. message: message,
  734. ackCode: success == true ? .genericSuccess : .genericFailure
  735. )
  736. }
  737. debug(.watchManager, "📱 Enacted bolus from watch via APS Manager: \(bolusDouble) U")
  738. // Notify Watch: "Carbs and bolus logged successfully"
  739. sendAcknowledgment(
  740. toWatch: true,
  741. message: String(
  742. localized: "Carbs and Bolus logged successfully.",
  743. comment: "Successful message sent to watch when logging carbs and bolus"
  744. ),
  745. ackCode: .comboComplete
  746. )
  747. } catch {
  748. debug(.watchManager, "❌ Error processing combined request: \(error)")
  749. sendAcknowledgment(toWatch: false, message: "Failed to log carbs and bolus", ackCode: .genericFailure)
  750. }
  751. }
  752. }
  753. private func handleCancelOverride() {
  754. Task {
  755. let context = CoreDataStack.shared.newTaskContext()
  756. if let overrideId = try await overrideStorage.fetchLatestActiveOverride() {
  757. let override = await context.perform {
  758. context.object(with: overrideId) as? OverrideStored
  759. }
  760. await context.perform {
  761. if let activeOverride = override {
  762. activeOverride.enabled = false
  763. do {
  764. guard context.hasChanges else {
  765. // Acknowledge failure
  766. self.sendAcknowledgment(
  767. toWatch: false,
  768. message: "Error! Something went wrong when processing your request.",
  769. ackCode: .genericFailure
  770. )
  771. return
  772. }
  773. try context.save()
  774. debug(.watchManager, "📱 Successfully stopped override")
  775. // Send notification to update Adjustments UI
  776. Foundation.NotificationCenter.default.post(
  777. name: .didUpdateOverrideConfiguration,
  778. object: nil
  779. )
  780. // Acknowledge cancellation success
  781. self.sendAcknowledgment(
  782. toWatch: true,
  783. message: String(
  784. localized: "Stopped Override successfully.",
  785. comment: "Stopped Override successfully"
  786. ),
  787. ackCode: .overrideStopped
  788. )
  789. } catch {
  790. debug(.watchManager, "❌ Error cancelling override: \(error)")
  791. // Acknowledge cancellation error
  792. self.sendAcknowledgment(toWatch: false, message: "Error stopping Override.", ackCode: .genericFailure)
  793. }
  794. }
  795. }
  796. } else {
  797. debug(.watchManager, "❌ No active override found.")
  798. self.sendAcknowledgment(
  799. toWatch: false,
  800. message: "No active override found.",
  801. ackCode: .genericFailure
  802. )
  803. return
  804. }
  805. }
  806. }
  807. private func handleActivateOverride(_ presetName: String) {
  808. Task {
  809. let context = CoreDataStack.shared.newTaskContext()
  810. debug(.watchManager, "📱 Fetching all override presets...")
  811. // Fetch all presets to find the one to activate
  812. let presetIds = try await overrideStorage.fetchForOverridePresets()
  813. let presets: [OverrideStored] = try await CoreDataStack.shared
  814. .getNSManagedObject(with: presetIds, context: context)
  815. debug(.watchManager, "📱 Checking for active override...")
  816. do {
  817. // Check for active override
  818. if let activeOverrideId = try await overrideStorage.fetchLatestActiveOverride() {
  819. let activeOverride = await context.perform {
  820. context.object(with: activeOverrideId) as? OverrideStored
  821. }
  822. // Deactivate, if necessary
  823. if let override = activeOverride {
  824. await context.perform {
  825. override.enabled = false
  826. }
  827. }
  828. } else {
  829. debug(.watchManager, "📱 Currently no override is active... proceeding to activate override: \(presetName)")
  830. }
  831. } catch {
  832. debug(.watchManager, "❌ Error while checking for active override: \(error)")
  833. self.sendAcknowledgment(
  834. toWatch: false,
  835. message: "Failed to load active override.",
  836. ackCode: .genericFailure
  837. )
  838. return
  839. }
  840. // Activate the selected preset
  841. await context.perform {
  842. guard let presetToActivate = presets
  843. .first(where: { $0.name?.trimmingCharacters(in: .whitespacesAndNewlines) == presetName })
  844. else {
  845. debug(.watchManager, "❌ No matching preset found for name: \"\(presetName)\" in \(presets.map(\.name))")
  846. self.sendAcknowledgment(
  847. toWatch: false,
  848. message: String(
  849. localized: "Preset \"\(presetName)\" not found.",
  850. comment: "Preset not found"
  851. ),
  852. ackCode: .genericFailure
  853. )
  854. return
  855. }
  856. presetToActivate.enabled = true
  857. presetToActivate.date = Date()
  858. do {
  859. guard context.hasChanges else {
  860. // Acknowledge failure
  861. self.sendAcknowledgment(
  862. toWatch: false,
  863. message: String(
  864. localized: "Error! Something went wrong when processing your request.",
  865. comment: "Error message when activating override"
  866. ),
  867. ackCode: .genericFailure
  868. )
  869. return
  870. }
  871. try context.save()
  872. debug(.watchManager, "📱 Successfully activated override: \(presetName)")
  873. // Send notification to update Adjustments UI
  874. Foundation.NotificationCenter.default.post(
  875. name: .didUpdateOverrideConfiguration,
  876. object: nil
  877. )
  878. // Acknowledge activation success
  879. self.sendAcknowledgment(
  880. toWatch: true,
  881. message: String(
  882. localized: "Started Override \"\(presetName)\" successfully.",
  883. comment: "Start override with override name"
  884. ),
  885. ackCode: .overrideStarted
  886. )
  887. } catch {
  888. debug(.watchManager, "❌ Error activating override: \(error)")
  889. // Acknowledge activation error
  890. self.sendAcknowledgment(
  891. toWatch: false,
  892. message: "Error activating Override \"\(presetName)\".",
  893. ackCode: .genericFailure
  894. )
  895. }
  896. }
  897. }
  898. }
  899. private func handleActivateTempTarget(_ presetName: String) {
  900. Task {
  901. let context = CoreDataStack.shared.newTaskContext()
  902. // Fetch all presets to find the one to activate
  903. let presetIds = try await tempTargetStorage.fetchForTempTargetPresets()
  904. let presets: [TempTargetStored] = try await CoreDataStack.shared
  905. .getNSManagedObject(with: presetIds, context: context)
  906. // Check for active temp target
  907. if let activeTempTargetId = try await tempTargetStorage.loadLatestTempTargetConfigurations(fetchLimit: 1).first {
  908. let activeTempTarget = await context.perform {
  909. context.object(with: activeTempTargetId) as? TempTargetStored
  910. }
  911. // Deactivate if exists
  912. if let tempTarget = activeTempTarget {
  913. await context.perform {
  914. tempTarget.enabled = false
  915. }
  916. }
  917. }
  918. // Activate the selected preset
  919. await context.perform {
  920. if let presetToActivate = presets.first(where: { $0.name == presetName }) {
  921. presetToActivate.enabled = true
  922. presetToActivate.date = Date()
  923. do {
  924. guard context.hasChanges else {
  925. // Acknowledge failure
  926. self.sendAcknowledgment(
  927. toWatch: false,
  928. message: "Error! Something went wrong when processing your request.",
  929. ackCode: .genericFailure
  930. )
  931. return
  932. }
  933. try context.save()
  934. debug(.watchManager, "📱 Successfully activated temp target: \(presetName)")
  935. let settingsHalfBasalTarget = self.settingsManager.preferences
  936. .halfBasalExerciseTarget
  937. let halfBasalTarget = presetToActivate.halfBasalTarget?.decimalValue
  938. // To activate the temp target also in oref
  939. let tempTarget = TempTarget(
  940. name: presetToActivate.name,
  941. createdAt: Date(),
  942. targetTop: presetToActivate.target?.decimalValue,
  943. targetBottom: presetToActivate.target?.decimalValue,
  944. duration: presetToActivate.duration?.decimalValue ?? 0,
  945. enteredBy: TempTarget.local,
  946. reason: TempTarget.custom,
  947. isPreset: true,
  948. enabled: true,
  949. halfBasalTarget: halfBasalTarget ?? settingsHalfBasalTarget
  950. )
  951. self.tempTargetStorage.saveTempTargetsToStorage([tempTarget])
  952. // Send notification to update Adjustments UI
  953. Foundation.NotificationCenter.default.post(
  954. name: .didUpdateTempTargetConfiguration,
  955. object: nil
  956. )
  957. // Acknowledge activation success
  958. self.sendAcknowledgment(
  959. toWatch: true,
  960. message: String(
  961. localized: "Started Temp Target \"\(presetName)\" successfully.",
  962. comment: "Started Temp Target successfully."
  963. ),
  964. ackCode: .tempTargetStarted
  965. )
  966. } catch {
  967. debug(.watchManager, "❌ Error activating temp target: \(error)")
  968. // Acknowledge activation error
  969. self.sendAcknowledgment(
  970. toWatch: false,
  971. message: "Error activating Temp Target \"\(presetName)\".",
  972. ackCode: .genericFailure
  973. )
  974. }
  975. }
  976. }
  977. }
  978. }
  979. private func handleCancelTempTarget() {
  980. Task {
  981. let context = CoreDataStack.shared.newTaskContext()
  982. if let tempTargetId = try await tempTargetStorage.loadLatestTempTargetConfigurations(fetchLimit: 1).first {
  983. let tempTarget = await context.perform {
  984. context.object(with: tempTargetId) as? TempTargetStored
  985. }
  986. await context.perform {
  987. if let activeTempTarget = tempTarget {
  988. activeTempTarget.enabled = false
  989. do {
  990. guard context.hasChanges else {
  991. // Acknowledge failure
  992. self.sendAcknowledgment(
  993. toWatch: false,
  994. message: "Error! Something went wrong when processing your request.",
  995. ackCode: .genericFailure
  996. )
  997. return
  998. }
  999. try context.save()
  1000. debug(.watchManager, "📱 Successfully cancelled temp target")
  1001. // To cancel the temp target also for oref
  1002. self.tempTargetStorage.saveTempTargetsToStorage([TempTarget.cancel(at: Date())])
  1003. // Send notification to update Adjustments UI
  1004. Foundation.NotificationCenter.default.post(
  1005. name: .didUpdateTempTargetConfiguration,
  1006. object: nil
  1007. )
  1008. // Acknowledge cancellation success
  1009. self.sendAcknowledgment(
  1010. toWatch: true,
  1011. message: String(
  1012. localized: "Stopped Temp Target successfully.",
  1013. comment: "Stopped Temp Target successfully."
  1014. ),
  1015. ackCode: .tempTargetStopped
  1016. )
  1017. } catch {
  1018. debug(.watchManager, "❌ Error stopping temp target: \(error)")
  1019. // Acknowledge cancellation error
  1020. self.sendAcknowledgment(
  1021. toWatch: false,
  1022. message: "Error stopping Temp Target.",
  1023. ackCode: .genericFailure
  1024. )
  1025. }
  1026. }
  1027. }
  1028. }
  1029. }
  1030. }
  1031. }
  1032. // TODO: - is there a better approach than setting up the watch state every time a setting has changed?
  1033. extension BaseWatchManager: SettingsObserver, PumpSettingsObserver {
  1034. // to update maxBolus
  1035. func pumpSettingsDidChange(_: PumpSettings) {
  1036. // Skip if no watch is paired or app not installed
  1037. guard let session = self.session, session.isPaired, session.isReachable, session.isWatchAppInstalled else { return }
  1038. Task {
  1039. let state = await self.setupWatchState()
  1040. await self.sendDataToWatch(state)
  1041. }
  1042. }
  1043. // to update the rest
  1044. func settingsDidChange(_: TrioSettings) {
  1045. units = settingsManager.settings.units
  1046. glucoseColorScheme = settingsManager.settings.glucoseColorScheme
  1047. lowGlucose = settingsManager.settings.low
  1048. highGlucose = settingsManager.settings.high
  1049. // Skip if no watch is paired or app not installed
  1050. guard let session = self.session, session.isPaired, session.isReachable, session.isWatchAppInstalled else { return }
  1051. Task {
  1052. let state = await self.setupWatchState()
  1053. await self.sendDataToWatch(state)
  1054. }
  1055. }
  1056. }
  1057. extension BaseWatchManager {
  1058. /// Retrieves the current glucose target based on the time of day.
  1059. private func getCurrentGlucoseTarget() async -> Decimal? {
  1060. let now = Date()
  1061. let calendar = Calendar.current
  1062. let bgTargets = await fileStorage.retrieveAsync(OpenAPS.Settings.bgTargets, as: BGTargets.self)
  1063. ?? BGTargets(from: OpenAPS.defaults(for: OpenAPS.Settings.bgTargets))
  1064. ?? BGTargets(units: .mgdL, userPreferredUnits: .mgdL, targets: [])
  1065. let entries: [(start: String, value: Decimal)] = bgTargets.targets.map { ($0.start, $0.low) }
  1066. for (index, entry) in entries.enumerated() {
  1067. guard let entryTime = TherapySettingsUtil.parseTime(entry.start) else {
  1068. debug(.default, "Invalid entry start time: \(entry.start)")
  1069. continue
  1070. }
  1071. let entryComponents = calendar.dateComponents([.hour, .minute, .second], from: entryTime)
  1072. let entryStartTime = calendar.date(
  1073. bySettingHour: entryComponents.hour!,
  1074. minute: entryComponents.minute!,
  1075. second: entryComponents.second!,
  1076. of: now
  1077. )!
  1078. let entryEndTime: Date
  1079. if index < entries.count - 1,
  1080. let nextEntryTime = TherapySettingsUtil.parseTime(entries[index + 1].start)
  1081. {
  1082. let nextEntryComponents = calendar.dateComponents([.hour, .minute, .second], from: nextEntryTime)
  1083. entryEndTime = calendar.date(
  1084. bySettingHour: nextEntryComponents.hour!,
  1085. minute: nextEntryComponents.minute!,
  1086. second: nextEntryComponents.second!,
  1087. of: now
  1088. )!
  1089. } else {
  1090. entryEndTime = calendar.date(byAdding: .day, value: 1, to: entryStartTime)!
  1091. }
  1092. if now >= entryStartTime, now < entryEndTime {
  1093. return entry.value
  1094. }
  1095. }
  1096. return nil
  1097. }
  1098. }
  1099. extension BaseWatchManager {
  1100. enum AcknowledgmentCode: String, Codable {
  1101. case savingCarbs = "saving_carbs"
  1102. case enactingBolus = "enacting_bolus"
  1103. case comboComplete = "combo_complete"
  1104. case carbsLogged = "carbs_logged"
  1105. case overrideStarted = "override_started"
  1106. case overrideStopped = "override_stopped"
  1107. case tempTargetStarted = "temp_target_started"
  1108. case tempTargetStopped = "temp_target_stopped"
  1109. case genericSuccess = "success"
  1110. case genericFailure = "failure"
  1111. }
  1112. }