AppleWatchManager.swift 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610
  1. import Combine
  2. import CoreData
  3. import Foundation
  4. import Swinject
  5. import WatchConnectivity
  6. /// Protocol defining the base functionality for Watch communication
  7. // TODO: Complete this
  8. protocol WatchManager {}
  9. /// Main implementation of the Watch communication manager
  10. /// Handles bidirectional communication between iPhone and Apple Watch
  11. final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchManager {
  12. private var session: WCSession?
  13. @Injected() private var glucoseStorage: GlucoseStorage!
  14. @Injected() private var apsManager: APSManager!
  15. @Injected() private var settingsManager: SettingsManager!
  16. @Injected() private var determinationStorage: DeterminationStorage!
  17. @Injected() private var overrideStorage: OverrideStorage!
  18. @Injected() private var tempTargetStorage: TempTargetsStorage!
  19. private var units: GlucoseUnits = .mgdL
  20. private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
  21. private var subscriptions = Set<AnyCancellable>()
  22. typealias PumpEvent = PumpEventStored.EventType
  23. let backgroundContext = CoreDataStack.shared.newTaskContext()
  24. let viewContext = CoreDataStack.shared.persistentContainer.viewContext
  25. init(resolver: Resolver) {
  26. super.init()
  27. injectServices(resolver)
  28. setupWatchSession()
  29. units = settingsManager.settings.units
  30. // Observer for OrefDetermination and adjustments
  31. coreDataPublisher =
  32. changedObjectsOnManagedObjectContextDidSavePublisher()
  33. .receive(on: DispatchQueue.global(qos: .background))
  34. .share()
  35. .eraseToAnyPublisher()
  36. // Observer for glucose and manual glucose
  37. glucoseStorage.updatePublisher
  38. .receive(on: DispatchQueue.global(qos: .background))
  39. .sink { [weak self] _ in
  40. guard let self = self else { return }
  41. Task {
  42. let state = await self.setupWatchState()
  43. self.sendDataToWatch(state)
  44. }
  45. }
  46. .store(in: &subscriptions)
  47. registerHandlers()
  48. }
  49. private func registerHandlers() {
  50. coreDataPublisher?.filterByEntityName("OrefDetermination").sink { [weak self] _ in
  51. guard let self = self else { return }
  52. Task {
  53. let state = await self.setupWatchState()
  54. self.sendDataToWatch(state)
  55. }
  56. }.store(in: &subscriptions)
  57. // Due to the Batch insert this only is used for observing Deletion of Glucose entries
  58. coreDataPublisher?.filterByEntityName("GlucoseStored").sink { [weak self] _ in
  59. guard let self = self else { return }
  60. Task {
  61. let state = await self.setupWatchState()
  62. self.sendDataToWatch(state)
  63. }
  64. }.store(in: &subscriptions)
  65. coreDataPublisher?.filterByEntityName("OverrideStored").sink { [weak self] _ in
  66. guard let self = self else { return }
  67. Task {
  68. let state = await self.setupWatchState()
  69. self.sendDataToWatch(state)
  70. }
  71. }.store(in: &subscriptions)
  72. coreDataPublisher?.filterByEntityName("TempTargetStored").sink { [weak self] _ in
  73. guard let self = self else { return }
  74. Task {
  75. let state = await self.setupWatchState()
  76. self.sendDataToWatch(state)
  77. }
  78. }.store(in: &subscriptions)
  79. }
  80. /// Sets up the WatchConnectivity session if the device supports it
  81. private func setupWatchSession() {
  82. if WCSession.isSupported() {
  83. let session = WCSession.default
  84. session.delegate = self
  85. session.activate()
  86. self.session = session
  87. print("📱 Phone session setup - isPaired: \(session.isPaired)")
  88. } else {
  89. print("📱 WCSession is not supported on this device")
  90. }
  91. }
  92. /// Attempts to reestablish the Watch connection if it becomes unreachable
  93. private func retryConnection() {
  94. guard let session = session else { return }
  95. if !session.isReachable {
  96. print("📱 Attempting to reactivate session...")
  97. session.activate()
  98. }
  99. }
  100. /// Prepares the current state data to be sent to the Watch
  101. /// - Returns: WatchState containing current glucose readings and trends and determination infos for displaying cob and iob in the view
  102. private func setupWatchState() async -> WatchState {
  103. // Get NSManagedObjectIDs
  104. let glucoseIds = await fetchGlucose()
  105. // TODO: - if we want that the watch immediately displays updated cob and iob values when entered via treatment view from phone, we would need to use a predicate here that also filters for NON-ENACTED Determinations
  106. let determinationIds = await determinationStorage.fetchLastDeterminationObjectID(
  107. predicate: NSPredicate.predicateFor30MinAgoForDetermination
  108. )
  109. let overridePresetIds = await overrideStorage.fetchForOverridePresets()
  110. let tempTargetPresetIds = await tempTargetStorage.fetchForTempTargetPresets()
  111. // Get NSManagedObjects
  112. let glucoseObjects: [GlucoseStored] = await CoreDataStack.shared
  113. .getNSManagedObject(with: glucoseIds, context: backgroundContext)
  114. let determinationObjects: [OrefDetermination] = await CoreDataStack.shared
  115. .getNSManagedObject(with: determinationIds, context: backgroundContext)
  116. let overridePresetObjects: [OverrideStored] = await CoreDataStack.shared
  117. .getNSManagedObject(with: overridePresetIds, context: backgroundContext)
  118. let tempTargetPresetObjects: [TempTargetStored] = await CoreDataStack.shared
  119. .getNSManagedObject(with: tempTargetPresetIds, context: backgroundContext)
  120. return await backgroundContext.perform {
  121. var watchState = WatchState()
  122. // Set lastLoopDate
  123. let lastLoopMinutes = Int((Date().timeIntervalSince(self.apsManager.lastLoopDate) - 30) / 60) + 1
  124. if lastLoopMinutes > 1440 {
  125. watchState.lastLoopTime = "--"
  126. } else {
  127. watchState.lastLoopTime = "\(lastLoopMinutes)" + NSLocalizedString("min", comment: "Minutes ago since last loop")
  128. }
  129. // Set IOB and COB from latest determination
  130. if let latestDetermination = determinationObjects.first {
  131. let iob = latestDetermination.iob ?? 0
  132. watchState.iob = Formatter.decimalFormatterWithTwoFractionDigits.string(from: iob)
  133. let cob = NSNumber(value: latestDetermination.cob)
  134. watchState.cob = Formatter.integerFormatter.string(from: cob)
  135. }
  136. // Set override presets with their enabled status
  137. watchState.overridePresets = overridePresetObjects.map { override in
  138. OverridePresetWatch(
  139. name: override.name ?? "",
  140. isEnabled: override.enabled
  141. )
  142. }
  143. guard let latestGlucose = glucoseObjects.first else {
  144. return watchState
  145. }
  146. // Map glucose values
  147. watchState.glucoseValues = glucoseObjects.compactMap { glucose in
  148. guard let date = glucose.date else { return nil }
  149. return (date: date, glucose: Double(glucose.glucose))
  150. }
  151. .sorted { $0.date < $1.date }
  152. // Set current glucose with proper formatting
  153. watchState.currentGlucose = "\(latestGlucose.glucose)"
  154. // Convert direction to trend string
  155. watchState.trend = latestGlucose.direction
  156. // Calculate delta if we have at least 2 readings
  157. if glucoseObjects.count >= 2 {
  158. let deltaValue = glucoseObjects[0].glucose - glucoseObjects[1].glucose
  159. let formattedDelta = Formatter.glucoseFormatter(for: self.units)
  160. .string(from: NSNumber(value: abs(deltaValue))) ?? "0"
  161. watchState.delta = deltaValue < 0 ? "-\(formattedDelta)" : "+\(formattedDelta)"
  162. }
  163. // Set temp target presets with their enabled status
  164. watchState.tempTargetPresets = tempTargetPresetObjects.map { tempTarget in
  165. TempTargetPresetWatch(
  166. name: tempTarget.name ?? "",
  167. isEnabled: tempTarget.enabled
  168. )
  169. }
  170. // Set units
  171. watchState.units = self.units
  172. print(
  173. "📱 Setup WatchState - currentGlucose: \(watchState.currentGlucose ?? "nil"), trend: \(watchState.trend ?? "nil"), delta: \(watchState.delta ?? "nil"), values: \(watchState.glucoseValues.count)"
  174. )
  175. return watchState
  176. }
  177. }
  178. /// Fetches recent glucose readings from CoreData
  179. /// - Returns: Array of NSManagedObjectIDs for glucose readings
  180. private func fetchGlucose() async -> [NSManagedObjectID] {
  181. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  182. ofType: GlucoseStored.self,
  183. onContext: backgroundContext,
  184. predicate: NSPredicate.glucose,
  185. key: "date",
  186. ascending: false,
  187. fetchLimit: 288
  188. )
  189. return await backgroundContext.perform {
  190. guard let fetchedResults = results as? [GlucoseStored] else { return [] }
  191. return fetchedResults.map(\.objectID)
  192. }
  193. }
  194. // MARK: - Send Data to Watch
  195. /// Sends the state of type WatchState to the connected Watch
  196. /// - Parameter state: Current WatchState containing glucose data to be sent
  197. func sendDataToWatch(_ state: WatchState) {
  198. guard let session = session, session.isReachable else {
  199. print("⌚️ Watch not reachable")
  200. return
  201. }
  202. let message: [String: Any] = [
  203. "currentGlucose": state.currentGlucose ?? "0",
  204. "trend": state.trend ?? "?",
  205. "delta": state.delta ?? "0",
  206. "glucoseValues": state.glucoseValues.map { value in
  207. [
  208. "glucose": value.glucose,
  209. "date": value.date.timeIntervalSince1970
  210. ]
  211. },
  212. "iob": state.iob ?? "0",
  213. "cob": state.cob ?? "0",
  214. "lastLoopTime": state.lastLoopTime ?? "--",
  215. "overridePresets": state.overridePresets.map { preset in
  216. [
  217. "name": preset.name,
  218. "isEnabled": preset.isEnabled
  219. ]
  220. },
  221. "tempTargetPresets": state.tempTargetPresets.map { preset in
  222. [
  223. "name": preset.name,
  224. "isEnabled": preset.isEnabled
  225. ]
  226. }
  227. ]
  228. print("📱 Sending to watch - Message content:")
  229. message.forEach { key, value in
  230. print("📱 \(key): \(value) (type: \(type(of: value)))")
  231. }
  232. session.sendMessage(message, replyHandler: nil) { error in
  233. print("❌ Error sending data: \(error.localizedDescription)")
  234. }
  235. }
  236. // MARK: - WCSessionDelegate
  237. func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
  238. if let error = error {
  239. print("📱 Phone session activation failed: \(error.localizedDescription)")
  240. return
  241. }
  242. print("📱 Phone session activated with state: \(activationState.rawValue)")
  243. print("📱 Phone isReachable after activation: \(session.isReachable)")
  244. // Try to send initial data after activation
  245. Task {
  246. let state = await self.setupWatchState()
  247. self.sendDataToWatch(state)
  248. }
  249. }
  250. func session(_: WCSession, didReceiveMessage message: [String: Any]) {
  251. DispatchQueue.main.async { [weak self] in
  252. if let bolusAmount = message["bolus"] as? Double,
  253. let isExternal = message["isExternal"] as? Bool
  254. {
  255. print("📱 Received \(isExternal ? "external insulin" : "bolus") request from watch: \(bolusAmount)U")
  256. if isExternal {
  257. self?.handleExternalInsulin(Decimal(bolusAmount))
  258. } else {
  259. self?.handleBolusRequest(Decimal(bolusAmount))
  260. }
  261. }
  262. if let carbsAmount = message["carbs"] as? Int,
  263. let timestamp = message["date"] as? TimeInterval
  264. {
  265. let date = Date(timeIntervalSince1970: timestamp)
  266. print("📱 Received carbs request from watch: \(carbsAmount)g at \(date)")
  267. self?.handleCarbsRequest(carbsAmount, date)
  268. }
  269. if message["cancelOverride"] as? Bool == true {
  270. print("📱 Received cancel override request from watch")
  271. self?.handleCancelOverride()
  272. }
  273. if let presetName = message["activateOverride"] as? String {
  274. print("📱 Received activate override request from watch for preset: \(presetName)")
  275. self?.handleActivateOverride(presetName)
  276. }
  277. if let presetName = message["activateTempTarget"] as? String {
  278. print("📱 Received activate temp target request from watch for preset: \(presetName)")
  279. self?.handleActivateTempTarget(presetName)
  280. }
  281. if message["cancelTempTarget"] as? Bool == true {
  282. print("📱 Received cancel temp target request from watch")
  283. self?.handleCancelTempTarget()
  284. }
  285. }
  286. }
  287. #if os(iOS)
  288. func sessionDidBecomeInactive(_: WCSession) {}
  289. func sessionDidDeactivate(_ session: WCSession) {
  290. session.activate()
  291. }
  292. #endif
  293. func sessionReachabilityDidChange(_ session: WCSession) {
  294. print("📱 Phone reachability changed: \(session.isReachable)")
  295. if session.isReachable {
  296. // Try to send data when connection is established
  297. Task {
  298. let state = await self.setupWatchState()
  299. self.sendDataToWatch(state)
  300. }
  301. } else {
  302. // Try to reconnect after a short delay
  303. DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
  304. self?.retryConnection()
  305. }
  306. }
  307. }
  308. /// Handles external insulin entries received from the Watch
  309. /// - Parameter amount: The insulin amount in units to be recorded
  310. private func handleExternalInsulin(_ amount: Decimal) {
  311. Task {
  312. let context = CoreDataStack.shared.newTaskContext()
  313. await context.perform {
  314. // Create Bolus
  315. let bolus = BolusStored(context: context)
  316. bolus.amount = amount as NSDecimalNumber
  317. bolus.isSMB = false
  318. bolus.isExternal = true
  319. // Create PumpEvent
  320. let pumpEvent = PumpEventStored(context: context)
  321. pumpEvent.id = UUID().uuidString
  322. pumpEvent.timestamp = Date()
  323. pumpEvent.type = PumpEvent.bolus.rawValue
  324. pumpEvent.bolus = bolus
  325. pumpEvent.isUploadedToNS = false
  326. pumpEvent.isUploadedToHealth = false
  327. pumpEvent.isUploadedToTidepool = false
  328. do {
  329. guard context.hasChanges else { return }
  330. try context.save()
  331. print("📱 Saved external insulin and pump event from watch: \(amount)U")
  332. } catch {
  333. print("❌ Error saving external insulin and pump event: \(error.localizedDescription)")
  334. }
  335. }
  336. }
  337. }
  338. /// Processes bolus requests received from the Watch
  339. /// - Parameter amount: The requested bolus amount in units
  340. private func handleBolusRequest(_ amount: Decimal) {
  341. Task {
  342. await apsManager.enactBolus(amount: Double(amount), isSMB: false)
  343. print("📱 Enacted bolus via APS Manager: \(amount)U")
  344. }
  345. }
  346. /// Handles carbs entry requests received from the Watch
  347. /// - Parameters:
  348. /// - amount: The carbs amount in grams
  349. /// - date: Timestamp for the carbs entry
  350. private func handleCarbsRequest(_ amount: Int, _ date: Date) {
  351. Task {
  352. let context = CoreDataStack.shared.newTaskContext()
  353. await context.perform {
  354. let carbs = CarbEntryStored(context: context)
  355. carbs.carbs = Double(truncating: amount as NSNumber)
  356. carbs.date = date
  357. // TODO: add FPU
  358. do {
  359. guard context.hasChanges else { return }
  360. try context.save()
  361. print("📱 Saved carbs from watch: \(amount)g at \(date)")
  362. } catch {
  363. print("❌ Error saving carbs: \(error.localizedDescription)")
  364. }
  365. }
  366. }
  367. }
  368. private func handleCancelOverride() {
  369. Task {
  370. let context = CoreDataStack.shared.newTaskContext()
  371. if let overrideId = await overrideStorage.fetchLatestActiveOverride() {
  372. let override = await context.perform {
  373. context.object(with: overrideId) as? OverrideStored
  374. }
  375. await context.perform {
  376. if let activeOverride = override {
  377. activeOverride.enabled = false
  378. do {
  379. guard context.hasChanges else { return }
  380. try context.save()
  381. print("📱 Successfully cancelled override")
  382. // Send notification to update Adjustments UI
  383. Foundation.NotificationCenter.default.post(
  384. name: .didUpdateOverrideConfiguration,
  385. object: nil
  386. )
  387. } catch {
  388. print("❌ Error cancelling override: \(error.localizedDescription)")
  389. }
  390. }
  391. }
  392. }
  393. }
  394. }
  395. private func handleActivateOverride(_ presetName: String) {
  396. Task {
  397. let context = CoreDataStack.shared.newTaskContext()
  398. // Fetch all presets to find the one to activate
  399. let presetIds = await overrideStorage.fetchForOverridePresets()
  400. let presets: [OverrideStored] = await CoreDataStack.shared
  401. .getNSManagedObject(with: presetIds, context: context)
  402. // Check for active override
  403. if let activeOverrideId = await overrideStorage.fetchLatestActiveOverride() {
  404. let activeOverride = await context.perform {
  405. context.object(with: activeOverrideId) as? OverrideStored
  406. }
  407. // Deactivate if exists
  408. if let override = activeOverride {
  409. await context.perform {
  410. override.enabled = false
  411. }
  412. }
  413. }
  414. // Activate the selected preset
  415. await context.perform {
  416. if let presetToActivate = presets.first(where: { $0.name == presetName }) {
  417. presetToActivate.enabled = true
  418. presetToActivate.date = Date()
  419. do {
  420. guard context.hasChanges else { return }
  421. try context.save()
  422. print("📱 Successfully activated override: \(presetName)")
  423. // Send notification to update Adjustments UI
  424. Foundation.NotificationCenter.default.post(
  425. name: .didUpdateOverrideConfiguration,
  426. object: nil
  427. )
  428. } catch {
  429. print("❌ Error activating override: \(error.localizedDescription)")
  430. }
  431. }
  432. }
  433. }
  434. }
  435. private func handleCancelTempTarget() {
  436. Task {
  437. let context = CoreDataStack.shared.newTaskContext()
  438. if let tempTargetId = await tempTargetStorage.loadLatestTempTargetConfigurations(fetchLimit: 1).first {
  439. let tempTarget = await context.perform {
  440. context.object(with: tempTargetId) as? TempTargetStored
  441. }
  442. await context.perform {
  443. if let activeTempTarget = tempTarget {
  444. activeTempTarget.enabled = false
  445. do {
  446. guard context.hasChanges else { return }
  447. try context.save()
  448. print("📱 Successfully cancelled temp target")
  449. // To cancel the temp target also for oref
  450. self.tempTargetStorage.saveTempTargetsToStorage([TempTarget.cancel(at: Date())])
  451. // Send notification to update Adjustments UI
  452. Foundation.NotificationCenter.default.post(
  453. name: .didUpdateTempTargetConfiguration,
  454. object: nil
  455. )
  456. } catch {
  457. print("❌ Error cancelling temp target: \(error.localizedDescription)")
  458. }
  459. }
  460. }
  461. }
  462. }
  463. }
  464. private func handleActivateTempTarget(_ presetName: String) {
  465. Task {
  466. let context = CoreDataStack.shared.newTaskContext()
  467. // Fetch all presets to find the one to activate
  468. let presetIds = await tempTargetStorage.fetchForTempTargetPresets()
  469. let presets: [TempTargetStored] = await CoreDataStack.shared
  470. .getNSManagedObject(with: presetIds, context: context)
  471. // Check for active temp target
  472. if let activeTempTargetId = await tempTargetStorage.loadLatestTempTargetConfigurations(fetchLimit: 1).first {
  473. let activeTempTarget = await context.perform {
  474. context.object(with: activeTempTargetId) as? TempTargetStored
  475. }
  476. // Deactivate if exists
  477. if let tempTarget = activeTempTarget {
  478. await context.perform {
  479. tempTarget.enabled = false
  480. }
  481. }
  482. }
  483. // Activate the selected preset
  484. await context.perform {
  485. if let presetToActivate = presets.first(where: { $0.name == presetName }) {
  486. presetToActivate.enabled = true
  487. presetToActivate.date = Date()
  488. do {
  489. guard context.hasChanges else { return }
  490. try context.save()
  491. print("📱 Successfully activated temp target: \(presetName)")
  492. // To activate the temp target also in oref
  493. let tempTarget = TempTarget(
  494. name: presetToActivate.name,
  495. createdAt: Date(),
  496. targetTop: presetToActivate.target?.decimalValue,
  497. targetBottom: presetToActivate.target?.decimalValue,
  498. duration: presetToActivate.duration?.decimalValue ?? 0,
  499. enteredBy: TempTarget.local,
  500. reason: TempTarget.custom,
  501. isPreset: true,
  502. enabled: true,
  503. halfBasalTarget: presetToActivate.halfBasalTarget?.decimalValue
  504. )
  505. self.tempTargetStorage.saveTempTargetsToStorage([tempTarget])
  506. // Send notification to update Adjustments UI
  507. Foundation.NotificationCenter.default.post(
  508. name: .didUpdateTempTargetConfiguration,
  509. object: nil
  510. )
  511. } catch {
  512. print("❌ Error activating temp target: \(error.localizedDescription)")
  513. }
  514. }
  515. }
  516. }
  517. }
  518. }