WatchState.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. import Foundation
  2. import SwiftUI
  3. import WatchConnectivity
  4. /// WatchState manages the communication between the Watch app and the iPhone app using WatchConnectivity.
  5. /// It handles glucose data synchronization and sending treatment requests (bolus, carbs) to the phone.
  6. @Observable final class WatchState: NSObject, WCSessionDelegate {
  7. // MARK: - Properties
  8. /// The WatchConnectivity session instance used for communication
  9. private var session: WCSession?
  10. /// Indicates if the paired iPhone is currently reachable
  11. var isReachable = false
  12. var currentGlucose: String = "--"
  13. var trend: String? = ""
  14. var delta: String? = "--"
  15. var glucoseValues: [(date: Date, glucose: Double, color: Color)] = []
  16. var cob: String? = "--"
  17. var iob: String? = "--"
  18. var lastLoopTime: String? = "--"
  19. var overridePresets: [OverridePresetWatch] = []
  20. var tempTargetPresets: [TempTargetPresetWatch] = []
  21. /// treatments inputs
  22. /// used to store carbs for combined meal-bolus-treatments
  23. var carbsAmount: Int = 0
  24. var fatAmount: Int = 0
  25. var proteinAmount: Int = 0
  26. var bolusAmount = 0.0
  27. var activeBolusAmount = 0.0
  28. var confirmationProgress = 0.0
  29. var bolusProgress: Double = 0.0
  30. var isBolusCanceled = false
  31. // Safety limits
  32. var maxBolus: Decimal = 10
  33. var maxCarbs: Decimal = 250
  34. var maxFat: Decimal = 250
  35. var maxProtein: Decimal = 250
  36. var maxIOB: Decimal = 0
  37. var maxCOB: Decimal = 120
  38. // acknowlegement handling
  39. var showCommsAnimation: Bool = false
  40. var showAcknowledgmentBanner: Bool = false
  41. var acknowledgementStatus: AcknowledgementStatus = .pending
  42. var acknowledgmentMessage: String = ""
  43. var shouldNavigateToRoot: Bool = true
  44. // Meal bolus-specific properties
  45. var mealBolusStep: MealBolusStep = .savingCarbs
  46. var isMealBolusCombo: Bool = false
  47. var showBolusProgressOverlay: Bool {
  48. (!showAcknowledgmentBanner || !showCommsAnimation || !showCommsAnimation) && bolusProgress > 0 && bolusProgress < 1.0 &&
  49. !isBolusCanceled
  50. }
  51. override init() {
  52. super.init()
  53. setupSession()
  54. }
  55. /// Configures the WatchConnectivity session if supported on the device
  56. private func setupSession() {
  57. if WCSession.isSupported() {
  58. let session = WCSession.default
  59. session.delegate = self
  60. session.activate()
  61. self.session = session
  62. } else {
  63. print("⌚️ WCSession is not supported on this device")
  64. }
  65. }
  66. // MARK: - Send Data to Phone
  67. /// Sends a bolus insulin request to the paired iPhone
  68. /// - Parameters:
  69. /// - amount: The insulin amount to be delivered
  70. func sendBolusRequest(_ amount: Decimal) {
  71. guard let session = session, session.isReachable else { return }
  72. isBolusCanceled = false // Reset canceled state when starting new bolus
  73. activeBolusAmount = Double(truncating: amount as NSNumber) // Set active bolus amount
  74. let message: [String: Any] = [
  75. "bolus": amount
  76. ]
  77. session.sendMessage(message, replyHandler: nil) { error in
  78. print("Error sending bolus request: \(error.localizedDescription)")
  79. }
  80. // Display pending communication animation
  81. showCommsAnimation = true
  82. }
  83. /// Sends a carbohydrate entry request to the paired iPhone
  84. /// - Parameters:
  85. /// - amount: The amount of carbs in grams
  86. /// - date: The timestamp for the carb entry (defaults to current time)
  87. func sendCarbsRequest(_ amount: Int, _ date: Date = Date()) {
  88. guard let session = session, session.isReachable else { return }
  89. let message: [String: Any] = [
  90. "carbs": amount,
  91. "date": date.timeIntervalSince1970
  92. ]
  93. session.sendMessage(message, replyHandler: nil) { error in
  94. print("Error sending carbs request: \(error.localizedDescription)")
  95. }
  96. // Display pending communication animation
  97. showCommsAnimation = true
  98. }
  99. /// Sends a meal and bolus insulin combo request to the paired iPhone
  100. /// - Parameters:
  101. /// - amount: The insulin amount to be delivered
  102. /// - isExternal: Indicates if the bolus is from an external source
  103. func sendMealBolusComboRequest(carbsAmount _: Decimal, bolusAmount: Decimal, _ date: Date = Date()) {
  104. guard let session = session, session.isReachable else { return }
  105. let message: [String: Any] = [
  106. "bolus": bolusAmount,
  107. "carbs": bolusAmount,
  108. "date": date.timeIntervalSince1970
  109. ]
  110. session.sendMessage(message, replyHandler: nil) { error in
  111. print("Error sending meal bolus combo request: \(error.localizedDescription)")
  112. }
  113. // Display pending communication animation
  114. showCommsAnimation = true
  115. isMealBolusCombo = true
  116. }
  117. func sendCancelOverrideRequest() {
  118. guard let session = session, session.isReachable else { return }
  119. let message: [String: Any] = [
  120. "cancelOverride": true
  121. ]
  122. session.sendMessage(message, replyHandler: nil) { error in
  123. print("⌚️ Error sending cancel override request: \(error.localizedDescription)")
  124. }
  125. // Display pending communication animation
  126. showCommsAnimation = true
  127. }
  128. func sendActivateOverrideRequest(presetName: String) {
  129. guard let session = session, session.isReachable else { return }
  130. let message: [String: Any] = [
  131. "activateOverride": presetName
  132. ]
  133. session.sendMessage(message, replyHandler: nil) { error in
  134. print("⌚️ Error sending activate override request: \(error.localizedDescription)")
  135. }
  136. // Display pending communication animation
  137. showCommsAnimation = true
  138. }
  139. func sendCancelTempTargetRequest() {
  140. guard let session = session, session.isReachable else { return }
  141. let message: [String: Any] = [
  142. "cancelTempTarget": true
  143. ]
  144. session.sendMessage(message, replyHandler: nil) { error in
  145. print("⌚️ Error sending cancel temp target request: \(error.localizedDescription)")
  146. }
  147. // Display pending communication animation
  148. showCommsAnimation = true
  149. }
  150. func sendActivateTempTargetRequest(presetName: String) {
  151. guard let session = session, session.isReachable else { return }
  152. let message: [String: Any] = [
  153. "activateTempTarget": presetName
  154. ]
  155. session.sendMessage(message, replyHandler: nil) { error in
  156. print("⌚️ Error sending activate temp target request: \(error.localizedDescription)")
  157. }
  158. // Display pending communication animation
  159. showCommsAnimation = true
  160. }
  161. func sendCancelBolusRequest() {
  162. isBolusCanceled = true
  163. guard let session = session, session.isReachable else { return }
  164. let message: [String: Any] = [
  165. "cancelBolus": true
  166. ]
  167. session.sendMessage(message, replyHandler: nil) { error in
  168. print("Error sending cancel bolus request: \(error.localizedDescription)")
  169. }
  170. // Reset when cancelled
  171. bolusProgress = 0
  172. activeBolusAmount = 0
  173. // Display pending communication animation
  174. showCommsAnimation = true
  175. }
  176. // MARK: – Handle Acknowledgement Messages FROM Phone
  177. private func handleAcknowledgment(success: Bool, message: String, isFinal: Bool = true) {
  178. if success {
  179. print("⌚️ Acknowledgment received: \(message)")
  180. showCommsAnimation = false // Hide progress animation
  181. acknowledgementStatus = .success
  182. acknowledgmentMessage = "\(message)"
  183. } else {
  184. print("⌚️ Acknowledgment failed: \(message)")
  185. showCommsAnimation = false // Hide progress animation
  186. acknowledgementStatus = .failure
  187. acknowledgmentMessage = "\(message)"
  188. }
  189. if isFinal {
  190. showAcknowledgmentBanner = true
  191. DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
  192. self.showAcknowledgmentBanner = false
  193. }
  194. }
  195. }
  196. // MARK: - WCSessionDelegate
  197. /// Called when the session has completed activation
  198. /// Updates the reachability status and logs the activation state
  199. func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
  200. DispatchQueue.main.async {
  201. if let error = error {
  202. print("⌚️ Watch session activation failed: \(error.localizedDescription)")
  203. return
  204. }
  205. print("⌚️ Watch session activated with state: \(activationState.rawValue)")
  206. self.isReachable = session.isReachable
  207. print("⌚️ Watch isReachable after activation: \(session.isReachable)")
  208. }
  209. }
  210. /// Handles incoming messages from the paired iPhone
  211. /// Updates local glucose data, trend, and delta information
  212. func session(_: WCSession, didReceiveMessage message: [String: Any]) {
  213. print("⌚️ Watch received message: \(message)")
  214. DispatchQueue.main.async { [weak self] in
  215. guard let self = self else { return }
  216. if let acknowledged = message["acknowledged"] as? Bool,
  217. let ackMessage = message["message"] as? String
  218. {
  219. switch ackMessage {
  220. case "Saving carbs...":
  221. self.isMealBolusCombo = true
  222. self.mealBolusStep = .savingCarbs
  223. self.showCommsAnimation = true
  224. self.handleAcknowledgment(success: acknowledged, message: ackMessage, isFinal: false)
  225. case "Enacting bolus...":
  226. self.isMealBolusCombo = true
  227. self.mealBolusStep = .enactingBolus
  228. self.showCommsAnimation = true
  229. self.handleAcknowledgment(success: acknowledged, message: ackMessage, isFinal: false)
  230. case "Carbs and bolus logged successfully":
  231. self.isMealBolusCombo = false
  232. self.handleAcknowledgment(success: acknowledged, message: ackMessage, isFinal: true)
  233. default:
  234. self.isMealBolusCombo = false
  235. self.handleAcknowledgment(success: acknowledged, message: ackMessage, isFinal: true)
  236. }
  237. }
  238. if let currentGlucose = message["currentGlucose"] as? String {
  239. self.currentGlucose = currentGlucose
  240. }
  241. if let trend = message["trend"] as? String {
  242. self.trend = trend
  243. }
  244. if let delta = message["delta"] as? String {
  245. self.delta = delta
  246. }
  247. if let iob = message["iob"] as? String {
  248. self.iob = iob
  249. }
  250. if let cob = message["cob"] as? String {
  251. self.cob = cob
  252. }
  253. if let lastLoopTime = message["lastLoopTime"] as? String {
  254. self.lastLoopTime = lastLoopTime
  255. }
  256. if let glucoseData = message["glucoseValues"] as? [[String: Any]] {
  257. self.glucoseValues = glucoseData.compactMap { data in
  258. guard let glucose = data["glucose"] as? Double,
  259. let timestamp = data["date"] as? TimeInterval,
  260. let colorString = data["color"] as? String
  261. else { return nil }
  262. return (
  263. Date(timeIntervalSince1970: timestamp),
  264. glucose,
  265. colorString.toColor() // Convert colorString to Color
  266. )
  267. }
  268. .sorted { $0.date < $1.date }
  269. }
  270. if let overrideData = message["overridePresets"] as? [[String: Any]] {
  271. self.overridePresets = overrideData.compactMap { data in
  272. guard let name = data["name"] as? String,
  273. let isEnabled = data["isEnabled"] as? Bool
  274. else { return nil }
  275. return OverridePresetWatch(name: name, isEnabled: isEnabled)
  276. }
  277. }
  278. if let tempTargetData = message["tempTargetPresets"] as? [[String: Any]] {
  279. self.tempTargetPresets = tempTargetData.compactMap { data in
  280. guard let name = data["name"] as? String,
  281. let isEnabled = data["isEnabled"] as? Bool
  282. else { return nil }
  283. return TempTargetPresetWatch(name: name, isEnabled: isEnabled)
  284. }
  285. }
  286. if let bolusProgress = message["bolusProgress"] as? Double {
  287. if !self.isBolusCanceled {
  288. self.bolusProgress = bolusProgress
  289. }
  290. }
  291. if let bolusWasCanceled = message["bolusCanceled"] as? Bool, bolusWasCanceled {
  292. self.bolusProgress = 0
  293. self.activeBolusAmount = 0
  294. return
  295. }
  296. // Debug print für die Safety Limits
  297. if let maxBolusValue = message["maxBolus"] {
  298. print("⌚️ Received maxBolus: \(maxBolusValue) of type \(type(of: maxBolusValue))")
  299. if let decimalValue = (maxBolusValue as? NSNumber)?.decimalValue {
  300. self.maxBolus = decimalValue
  301. print("⌚️ Converted maxBolus to: \(decimalValue)")
  302. }
  303. }
  304. if let maxCarbsValue = message["maxCarbs"] {
  305. if let decimalValue = (maxCarbsValue as? NSNumber)?.decimalValue {
  306. self.maxCarbs = decimalValue
  307. }
  308. }
  309. if let maxFatValue = message["maxFat"] {
  310. if let decimalValue = (maxFatValue as? NSNumber)?.decimalValue {
  311. self.maxFat = decimalValue
  312. }
  313. }
  314. if let maxProteinValue = message["maxProtein"] {
  315. if let decimalValue = (maxProteinValue as? NSNumber)?.decimalValue {
  316. self.maxProtein = decimalValue
  317. }
  318. }
  319. if let maxIOBValue = message["maxIOB"] {
  320. if let decimalValue = (maxIOBValue as? NSNumber)?.decimalValue {
  321. self.maxIOB = decimalValue
  322. }
  323. }
  324. if let maxCOBValue = message["maxCOB"] {
  325. if let decimalValue = (maxCOBValue as? NSNumber)?.decimalValue {
  326. self.maxCOB = decimalValue
  327. }
  328. }
  329. }
  330. }
  331. /// Called when the reachability status of the paired iPhone changes
  332. /// Updates the local reachability status
  333. func sessionReachabilityDidChange(_ session: WCSession) {
  334. DispatchQueue.main.async {
  335. self.isReachable = session.isReachable
  336. print("⌚️ Watch reachability changed: \(session.isReachable)")
  337. }
  338. }
  339. }