AppleWatchManager.swift 56 KB

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