WatchState.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  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. /// treatments inputs
  21. /// used to store carbs for combined meal-bolus-treatments
  22. var carbsAmount: Int = 0
  23. var fatAmount: Int = 0
  24. var proteinAmount: Int = 0
  25. var bolusAmount = 0.0
  26. var activeBolusAmount = 0.0
  27. var confirmationProgress = 0.0
  28. var bolusProgress: Double = 0.0
  29. var isBolusCanceled = false
  30. // Safety limits
  31. var maxBolus: Decimal = 10
  32. var maxCarbs: Decimal = 250
  33. var maxFat: Decimal = 250
  34. var maxProtein: Decimal = 250
  35. var maxIOB: Decimal = 0
  36. var maxCOB: Decimal = 120
  37. override init() {
  38. super.init()
  39. setupSession()
  40. }
  41. /// Configures the WatchConnectivity session if supported on the device
  42. private func setupSession() {
  43. if WCSession.isSupported() {
  44. let session = WCSession.default
  45. session.delegate = self
  46. session.activate()
  47. self.session = session
  48. } else {
  49. print("⌚️ WCSession is not supported on this device")
  50. }
  51. }
  52. // MARK: - Send Data to Phone
  53. /// Sends a bolus insulin request to the paired iPhone
  54. /// - Parameters:
  55. /// - amount: The insulin amount to be delivered
  56. func sendBolusRequest(_ amount: Decimal) {
  57. guard let session = session, session.isReachable else { return }
  58. isBolusCanceled = false // Reset canceled state when starting new bolus
  59. activeBolusAmount = Double(truncating: amount as NSNumber) // Set active bolus amount
  60. let message: [String: Any] = [
  61. "bolus": amount
  62. ]
  63. session.sendMessage(message, replyHandler: nil) { error in
  64. print("Error sending bolus request: \(error.localizedDescription)")
  65. }
  66. }
  67. /// Sends a carbohydrate entry request to the paired iPhone
  68. /// - Parameters:
  69. /// - amount: The amount of carbs in grams
  70. /// - date: The timestamp for the carb entry (defaults to current time)
  71. func sendCarbsRequest(_ amount: Int, _ date: Date = Date()) {
  72. guard let session = session, session.isReachable else { return }
  73. let message: [String: Any] = [
  74. "carbs": amount,
  75. "date": date.timeIntervalSince1970
  76. ]
  77. session.sendMessage(message, replyHandler: nil) { error in
  78. print("Error sending carbs request: \(error.localizedDescription)")
  79. }
  80. }
  81. /// Sends a meal and bolus insulin combo request to the paired iPhone
  82. /// - Parameters:
  83. /// - amount: The insulin amount to be delivered
  84. /// - isExternal: Indicates if the bolus is from an external source
  85. func sendMealBolusComboRequest(carbsAmount _: Decimal, bolusAmount: Decimal, _ date: Date = Date()) {
  86. guard let session = session, session.isReachable else { return }
  87. let message: [String: Any] = [
  88. "bolus": bolusAmount,
  89. "carbs": bolusAmount,
  90. "date": date.timeIntervalSince1970
  91. ]
  92. session.sendMessage(message, replyHandler: nil) { error in
  93. print("Error sending meal bolus combo request: \(error.localizedDescription)")
  94. }
  95. }
  96. func sendCancelOverrideRequest() {
  97. guard let session = session, session.isReachable else { return }
  98. let message: [String: Any] = [
  99. "cancelOverride": true
  100. ]
  101. session.sendMessage(message, replyHandler: nil) { error in
  102. print("⌚️ Error sending cancel override request: \(error.localizedDescription)")
  103. }
  104. }
  105. func sendActivateOverrideRequest(presetName: String) {
  106. guard let session = session, session.isReachable else { return }
  107. let message: [String: Any] = [
  108. "activateOverride": presetName
  109. ]
  110. session.sendMessage(message, replyHandler: nil) { error in
  111. print("⌚️ Error sending activate override request: \(error.localizedDescription)")
  112. }
  113. }
  114. func sendCancelTempTargetRequest() {
  115. guard let session = session, session.isReachable else { return }
  116. let message: [String: Any] = [
  117. "cancelTempTarget": true
  118. ]
  119. session.sendMessage(message, replyHandler: nil) { error in
  120. print("⌚️ Error sending cancel temp target request: \(error.localizedDescription)")
  121. }
  122. }
  123. func sendActivateTempTargetRequest(presetName: String) {
  124. guard let session = session, session.isReachable else { return }
  125. let message: [String: Any] = [
  126. "activateTempTarget": presetName
  127. ]
  128. session.sendMessage(message, replyHandler: nil) { error in
  129. print("⌚️ Error sending activate temp target request: \(error.localizedDescription)")
  130. }
  131. }
  132. func sendCancelBolusRequest() {
  133. isBolusCanceled = true
  134. guard let session = session, session.isReachable else { return }
  135. let message: [String: Any] = [
  136. "cancelBolus": true
  137. ]
  138. session.sendMessage(message, replyHandler: nil) { error in
  139. print("Error sending cancel bolus request: \(error.localizedDescription)")
  140. }
  141. }
  142. // MARK: - WCSessionDelegate
  143. /// Called when the session has completed activation
  144. /// Updates the reachability status and logs the activation state
  145. func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
  146. DispatchQueue.main.async {
  147. if let error = error {
  148. print("⌚️ Watch session activation failed: \(error.localizedDescription)")
  149. return
  150. }
  151. print("⌚️ Watch session activated with state: \(activationState.rawValue)")
  152. self.isReachable = session.isReachable
  153. print("⌚️ Watch isReachable after activation: \(session.isReachable)")
  154. }
  155. }
  156. /// Handles incoming messages from the paired iPhone
  157. /// Updates local glucose data, trend, and delta information
  158. func session(_: WCSession, didReceiveMessage message: [String: Any]) {
  159. print("⌚️ Watch received message: \(message)")
  160. DispatchQueue.main.async { [weak self] in
  161. guard let self = self else { return }
  162. if let currentGlucose = message["currentGlucose"] as? String {
  163. self.currentGlucose = currentGlucose
  164. }
  165. if let trend = message["trend"] as? String {
  166. self.trend = trend
  167. }
  168. if let delta = message["delta"] as? String {
  169. self.delta = delta
  170. }
  171. if let iob = message["iob"] as? String {
  172. self.iob = iob
  173. }
  174. if let cob = message["cob"] as? String {
  175. self.cob = cob
  176. }
  177. if let lastLoopTime = message["lastLoopTime"] as? String {
  178. self.lastLoopTime = lastLoopTime
  179. }
  180. if let glucoseData = message["glucoseValues"] as? [[String: Any]] {
  181. self.glucoseValues = glucoseData.compactMap { data in
  182. guard let glucose = data["glucose"] as? Double,
  183. let timestamp = data["date"] as? TimeInterval
  184. else { return nil }
  185. return (Date(timeIntervalSince1970: timestamp), glucose)
  186. }
  187. .sorted { $0.date < $1.date }
  188. }
  189. if let overrideData = message["overridePresets"] as? [[String: Any]] {
  190. self.overridePresets = overrideData.compactMap { data in
  191. guard let name = data["name"] as? String,
  192. let isEnabled = data["isEnabled"] as? Bool
  193. else { return nil }
  194. return OverridePresetWatch(name: name, isEnabled: isEnabled)
  195. }
  196. }
  197. if let tempTargetData = message["tempTargetPresets"] as? [[String: Any]] {
  198. self.tempTargetPresets = tempTargetData.compactMap { data in
  199. guard let name = data["name"] as? String,
  200. let isEnabled = data["isEnabled"] as? Bool
  201. else { return nil }
  202. return TempTargetPresetWatch(name: name, isEnabled: isEnabled)
  203. }
  204. }
  205. if let bolusProgress = message["bolusProgress"] as? Double {
  206. if !self.isBolusCanceled {
  207. self.bolusProgress = bolusProgress
  208. }
  209. }
  210. // Debug print für die Safety Limits
  211. if let maxBolusValue = message["maxBolus"] {
  212. print("⌚️ Received maxBolus: \(maxBolusValue) of type \(type(of: maxBolusValue))")
  213. if let decimalValue = (maxBolusValue as? NSNumber)?.decimalValue {
  214. self.maxBolus = decimalValue
  215. print("⌚️ Converted maxBolus to: \(decimalValue)")
  216. }
  217. }
  218. if let maxCarbsValue = message["maxCarbs"] {
  219. if let decimalValue = (maxCarbsValue as? NSNumber)?.decimalValue {
  220. self.maxCarbs = decimalValue
  221. }
  222. }
  223. if let maxFatValue = message["maxFat"] {
  224. if let decimalValue = (maxFatValue as? NSNumber)?.decimalValue {
  225. self.maxFat = decimalValue
  226. }
  227. }
  228. if let maxProteinValue = message["maxProtein"] {
  229. if let decimalValue = (maxProteinValue as? NSNumber)?.decimalValue {
  230. self.maxProtein = decimalValue
  231. }
  232. }
  233. if let maxIOBValue = message["maxIOB"] {
  234. if let decimalValue = (maxIOBValue as? NSNumber)?.decimalValue {
  235. self.maxIOB = decimalValue
  236. }
  237. }
  238. if let maxCOBValue = message["maxCOB"] {
  239. if let decimalValue = (maxCOBValue as? NSNumber)?.decimalValue {
  240. self.maxCOB = decimalValue
  241. }
  242. }
  243. }
  244. }
  245. /// Called when the reachability status of the paired iPhone changes
  246. /// Updates the local reachability status
  247. func sessionReachabilityDidChange(_ session: WCSession) {
  248. DispatchQueue.main.async {
  249. self.isReachable = session.isReachable
  250. print("⌚️ Watch reachability changed: \(session.isReachable)")
  251. }
  252. }
  253. }