WatchState.swift 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. import Foundation
  2. import WatchConnectivity
  3. /// WatchState manages the communication between the Watch app and the iPhone app using WatchConnectivity.
  4. /// It handles glucose data synchronization and sending treatment requests (bolus, carbs) to the phone.
  5. @Observable final class WatchState: NSObject, WCSessionDelegate {
  6. // MARK: - Properties
  7. /// The WatchConnectivity session instance used for communication
  8. private var session: WCSession?
  9. /// Indicates if the paired iPhone is currently reachable
  10. var isReachable = false
  11. var currentGlucose: String = "--"
  12. var trend: String? = ""
  13. var delta: String? = "--"
  14. var glucoseValues: [(date: Date, glucose: Double)] = []
  15. var cob: String? = "--"
  16. var iob: String? = "--"
  17. var lastLoopTime: String? = "--"
  18. var overridePresets: [OverridePresetWatch] = []
  19. var tempTargetPresets: [TempTargetPresetWatch] = []
  20. override init() {
  21. super.init()
  22. setupSession()
  23. }
  24. /// Configures the WatchConnectivity session if supported on the device
  25. private func setupSession() {
  26. if WCSession.isSupported() {
  27. let session = WCSession.default
  28. session.delegate = self
  29. session.activate()
  30. self.session = session
  31. } else {
  32. print("⌚️ WCSession is not supported on this device")
  33. }
  34. }
  35. // MARK: - Send Data to Phone
  36. /// Sends a bolus insulin request to the paired iPhone
  37. /// - Parameters:
  38. /// - amount: The insulin amount to be delivered
  39. /// - isExternal: Indicates if the bolus is from an external source
  40. func sendBolusRequest(_ amount: Decimal, isExternal: Bool) {
  41. guard let session = session, session.isReachable else { return }
  42. let message: [String: Any] = [
  43. "bolus": amount,
  44. "isExternal": isExternal
  45. ]
  46. session.sendMessage(message, replyHandler: nil) { error in
  47. print("Error sending bolus request: \(error.localizedDescription)")
  48. }
  49. }
  50. /// Sends a carbohydrate entry request to the paired iPhone
  51. /// - Parameters:
  52. /// - amount: The amount of carbs in grams
  53. /// - date: The timestamp for the carb entry (defaults to current time)
  54. func sendCarbsRequest(_ amount: Int, _ date: Date = Date()) {
  55. guard let session = session, session.isReachable else { return }
  56. let message: [String: Any] = [
  57. "carbs": amount,
  58. "date": date.timeIntervalSince1970
  59. ]
  60. session.sendMessage(message, replyHandler: nil) { error in
  61. print("Error sending carbs request: \(error.localizedDescription)")
  62. }
  63. }
  64. func sendCancelOverrideRequest() {
  65. guard let session = session, session.isReachable else { return }
  66. let message: [String: Any] = [
  67. "cancelOverride": true
  68. ]
  69. session.sendMessage(message, replyHandler: nil) { error in
  70. print("⌚️ Error sending cancel override request: \(error.localizedDescription)")
  71. }
  72. }
  73. func sendActivateOverrideRequest(presetName: String) {
  74. guard let session = session, session.isReachable else { return }
  75. let message: [String: Any] = [
  76. "activateOverride": presetName
  77. ]
  78. session.sendMessage(message, replyHandler: nil) { error in
  79. print("⌚️ Error sending activate override request: \(error.localizedDescription)")
  80. }
  81. }
  82. func sendCancelTempTargetRequest() {
  83. guard let session = session, session.isReachable else { return }
  84. let message: [String: Any] = [
  85. "cancelTempTarget": true
  86. ]
  87. session.sendMessage(message, replyHandler: nil) { error in
  88. print("⌚️ Error sending cancel temp target request: \(error.localizedDescription)")
  89. }
  90. }
  91. func sendActivateTempTargetRequest(presetName: String) {
  92. guard let session = session, session.isReachable else { return }
  93. let message: [String: Any] = [
  94. "activateTempTarget": presetName
  95. ]
  96. session.sendMessage(message, replyHandler: nil) { error in
  97. print("⌚️ Error sending activate temp target request: \(error.localizedDescription)")
  98. }
  99. }
  100. // MARK: - WCSessionDelegate
  101. /// Called when the session has completed activation
  102. /// Updates the reachability status and logs the activation state
  103. func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
  104. DispatchQueue.main.async {
  105. if let error = error {
  106. print("⌚️ Watch session activation failed: \(error.localizedDescription)")
  107. return
  108. }
  109. print("⌚️ Watch session activated with state: \(activationState.rawValue)")
  110. self.isReachable = session.isReachable
  111. print("⌚️ Watch isReachable after activation: \(session.isReachable)")
  112. }
  113. }
  114. /// Handles incoming messages from the paired iPhone
  115. /// Updates local glucose data, trend, and delta information
  116. func session(_: WCSession, didReceiveMessage message: [String: Any]) {
  117. print("⌚️ Watch received message: \(message)")
  118. DispatchQueue.main.async { [weak self] in
  119. guard let self = self else { return }
  120. if let currentGlucose = message["currentGlucose"] as? String {
  121. self.currentGlucose = currentGlucose
  122. }
  123. if let trend = message["trend"] as? String {
  124. self.trend = trend
  125. }
  126. if let delta = message["delta"] as? String {
  127. self.delta = delta
  128. }
  129. if let iob = message["iob"] as? String {
  130. self.iob = iob
  131. }
  132. if let cob = message["cob"] as? String {
  133. self.cob = cob
  134. }
  135. if let lastLoopTime = message["lastLoopTime"] as? String {
  136. self.lastLoopTime = lastLoopTime
  137. }
  138. if let glucoseData = message["glucoseValues"] as? [[String: Any]] {
  139. self.glucoseValues = glucoseData.compactMap { data in
  140. guard let glucose = data["glucose"] as? Double,
  141. let timestamp = data["date"] as? TimeInterval
  142. else { return nil }
  143. return (Date(timeIntervalSince1970: timestamp), glucose)
  144. }
  145. .sorted { $0.date < $1.date }
  146. }
  147. if let overrideData = message["overridePresets"] as? [[String: Any]] {
  148. self.overridePresets = overrideData.compactMap { data in
  149. guard let name = data["name"] as? String,
  150. let isEnabled = data["isEnabled"] as? Bool
  151. else { return nil }
  152. return OverridePresetWatch(name: name, isEnabled: isEnabled)
  153. }
  154. }
  155. if let tempTargetData = message["tempTargetPresets"] as? [[String: Any]] {
  156. self.tempTargetPresets = tempTargetData.compactMap { data in
  157. guard let name = data["name"] as? String,
  158. let isEnabled = data["isEnabled"] as? Bool
  159. else { return nil }
  160. return TempTargetPresetWatch(name: name, isEnabled: isEnabled)
  161. }
  162. }
  163. }
  164. }
  165. /// Called when the reachability status of the paired iPhone changes
  166. /// Updates the local reachability status
  167. func sessionReachabilityDidChange(_ session: WCSession) {
  168. DispatchQueue.main.async {
  169. self.isReachable = session.isReachable
  170. print("⌚️ Watch reachability changed: \(session.isReachable)")
  171. }
  172. }
  173. }