// // WCManager.swift // meterios // // (c) 2025 Martin "maride" Dessauer // import SwiftUI import WatchConnectivity enum WCMeteriosMessageTypes: String { case DrinkList case BookDrink } class WCManager: NSObject, WCSessionDelegate { @AppStorage("meteHostAddr") private var meteHostAddr = "" @AppStorage("meteUserID") private var meteUserID = -1 private let session: WCSession = WCSession.default override init() { super.init() if (WCSession.isSupported()) { session.delegate = self session.activate() } } // Functions required by WCSessionDelegate protocol, but not of interest for Meterios #if os(iOS) func sessionDidBecomeInactive(_ session: WCSession) {} func sessionDidDeactivate(_ session: WCSession) {} #endif // os(iOS) func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: (any Error)?) {} // session(...) gets called for every incoming message through the WatchConnectivity channel // As Meterios currently only needs messages from watchOS to iOS and responses back to the watch, this function currently only implements iOS-related responses. func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) { #if os(iOS) // Check for the different possible message types switch message["request"] as! String { case WCMeteriosMessageTypes.DrinkList.rawValue: // Request for a list of available drinks Task { let drinks = try await BackendConnector().GetDrinks(baseAddr: meteHostAddr) let favouriteDrinks = drinks.filter({ FavouritesManager.default.IsFavourite($0.id) && $0.active }) let availableDrinks = drinks.filter({ !FavouritesManager.default.IsFavourite($0.id) && $0.active }) // note that drinks which are not active/available are not sent to the watch - the screen is too small to waste pixels. // Create a plist-compatible variant of the Drink object containing the required information let favouriteDrinkArray = favouriteDrinks.map { drink in try! JSONEncoder().encode(drink).base64EncodedString() } let availableDrinkArray = availableDrinks.map { drink in try! JSONEncoder().encode(drink).base64EncodedString() } replyHandler([ "favouriteDrinks": favouriteDrinkArray, "availableDrinks": availableDrinkArray ]) } case WCMeteriosMessageTypes.BookDrink.rawValue: // Book the given drink at the backend Task { do { // Book let drinkID = message["drinkID"] as! Int try await BackendConnector().BookDrink(baseAddr: meteHostAddr, userID: meteUserID, drinkID: drinkID) replyHandler([ "salut": SalutManager.getSalute() ]) } catch { // Error booking against the backend replyHandler([ "error": error, "salut": "" ]) } } default: // Malformed message from watch replyHandler([ "error": "request contains no request", ]) } #endif // os(iOS) } // requestDrinksList requests the list of drinks available at the backend from the paired device func requestDrinksList(replyHandler: @escaping (Array, Array) -> Void, errorHandler: @escaping ((any Error)?) -> Void) { session.sendMessage([ "request": WCMeteriosMessageTypes.DrinkList.rawValue ], replyHandler: { reply in let favouriteDrinks = reply["favouriteDrinks"] as! Array let availableDrinks = reply["availableDrinks"] as! Array // Create Drink objects from response let favouriteDrinksArray = favouriteDrinks.map { drink in try! JSONDecoder().decode(Drink.self, from: Data(base64Encoded: drink)!) } let availableDrinksArray = availableDrinks.map { drink in try! JSONDecoder().decode(Drink.self, from: Data(base64Encoded: drink)!) } // enjoy your drinks, replyHandler! replyHandler(favouriteDrinksArray, availableDrinksArray) }, errorHandler: { error in errorHandler(error) }) } // bookDrink books the drink given by drinkID func bookDrink(drinkID: Int, replyHandler: @escaping (String) -> Void, errorHandler: @escaping ((any Error)?) -> Void) { session.sendMessage([ "request": WCMeteriosMessageTypes.BookDrink.rawValue, "drinkID": drinkID, ], replyHandler: { reply in replyHandler(reply["salut"] as! String) }, errorHandler: { error in errorHandler(error) }) } }