// // WemoServer.swift // XIONControlPanel // // Created by Charles Magahern on 12/31/15. // Copyright © 2015 XION. All rights reserved. // import Foundation enum ConnectionError : Error { case unknown case serverUnavailable } class WemoServer : Server { private var devices: [WemoDevice] = [] public var connectionStatus: ConnectionStatus = .disconnected public weak var delegate: ServerDelegate? fileprivate(set) var baseURL: URL fileprivate var _urlSession: URLSession fileprivate var _errorStream: StandardErrorOutputStream = StandardErrorOutputStream() fileprivate var _operationQueue: OperationQueue = OperationQueue() required init(_ url: URL) { self.baseURL = url let config = URLSessionConfiguration.default _urlSession = URLSession(configuration: config) _operationQueue.maxConcurrentOperationCount = 1 } func connect(_ completion: @escaping (Error?) -> Void) { if (self.connectionStatus == .disconnected) { self.connectionStatus = .connecting let op = ConnectOperation(baseURL: self.baseURL, session: _urlSession) op.completionBlock = { [weak op] in guard let strongOp = op else { completion(nil) ; return } if let error = strongOp.error { self._logError("Error connecting to server", error: error) self.connectionStatus = .disconnected } else { self.connectionStatus = .connected } completion(strongOp.error) } _operationQueue.addOperation(op) } else { completion(nil) } } func disconnect(_ completion: (Error?) -> Void) { self.connectionStatus = .disconnected completion(nil) } func fetchDevices(_ completion: @escaping (Result<[AnyDevice], Error>) -> Void) { let op = FetchDevicesOperation(baseURL: self.baseURL, session: _urlSession) op.completionBlock = { [weak op] in guard let op else { completion(.failure(ConnectionError.unknown)) return } if let error = op.error { self._logError("Error fetching devices", error: error) completion(.failure(error)) return } self.devices = op.devices completion(.success(self.devices.map { AnyDevice($0) })) } if self.connectionStatus == .disconnected { connect { error in if error == nil { self._operationQueue.addOperation(op) } } } else if self.connectionStatus == .connected { _operationQueue.addOperation(op) } else { completion(.failure(ConnectionError.serverUnavailable)) } } func toggleDevice(_ device: AnyDevice, state: DeviceState, completion: @escaping (Error?) -> Void) { if connectionStatus == .connected, let device = findDevice(device) { let op = ToggleDeviceOperation(baseURL: self.baseURL, session: _urlSession, device: device, state: state) op.completionBlock = { [weak op] in guard let strongOp = op else { completion(nil) ; return } if let error = strongOp.error { self._logError("Error toggling device", error: error) } completion(strongOp.error) } _operationQueue.addOperation(op) } else { let err = ConnectionError.serverUnavailable completion(err) } } func responsibleForDevice(_ device: AnyDevice) -> Bool { return self.devices.contains { $0.serial == device.serial } } // MARK: Internal internal func _logError(_ description: String, error: Error) { print("ERROR: \(description) \(error)", to: &_errorStream) } internal func findDevice(_ device: AnyDevice) -> WemoDevice? { return self.devices.first { $0.serial == device.serial } } } internal class WemoOperation : Operation, @unchecked Sendable { var baseURL: URL var session: URLSession fileprivate(set) var error: Error? init(baseURL: URL, session: URLSession) { self.baseURL = baseURL self.session = session } } internal class ConnectOperation : WemoOperation, @unchecked Sendable { override func main() { let semaphore = Semaphore(value: 0) let url = self.baseURL.appendingPathComponent("api/environment") var request = URLRequest(url: url) request.httpMethod = "POST" let task = self.session.dataTask(with: request, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) -> Void in self.error = error semaphore.signal() }) task.resume() semaphore.wait() } } internal class FetchDevicesOperation : WemoOperation, @unchecked Sendable { private(set) var devices: [WemoDevice] = [] override func main() { let semaphore = Semaphore(value: 0) let url = self.baseURL.appendingPathComponent("api/environment") let task = self.session.dataTask(with: url, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) -> Void in if (data != nil) { self.devices = self._parseDevices(data!) } else { self.error = error } semaphore.signal() }) task.resume() semaphore.wait() } internal func _parseDevices(_ data: Data) -> [WemoDevice] { var devices: [WemoDevice] = [] if let responseDict = try? JSONSerialization.jsonObject(with: data, options: []) as? NSDictionary { for responseObj in responseDict.allValues { if let deviceDict = responseObj as? NSDictionary { let device = WemoDevice(deviceDict) devices.append(device) } } } return devices } } internal class ToggleDeviceOperation : WemoOperation, @unchecked Sendable { var device: WemoDevice var state: DeviceState init(baseURL: URL, session: URLSession, device: WemoDevice, state: DeviceState) { self.device = device self.state = state super.init(baseURL: baseURL, session: session) } override func main() { let semaphore = Semaphore(value: 0) let stateArg = (self.state == .on ? "on" : "off") let url = self.baseURL.appendingPathComponent("api/device/\(self.device.name)").URLByAppendingRequestParameters(["state" : stateArg]) var request = URLRequest(url: url!) request.httpMethod = "POST" let task = self.session.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in self.error = error semaphore.signal() } task.resume() semaphore.wait() } }