// // HubitatServer.swift // XIONControlPanel // // Created by James Magahern on 3/14/20. // Copyright © 2020 XION. All rights reserved. // import Foundation import Combine public enum HubitatServerError : Error { case apiError } public class HubitatServer : Server { var connectionStatus: ConnectionStatus = .disconnected public fileprivate(set) var devices: [HubitatDevice] = [] private let baseURL: URL private let accessToken = "fa861583-71f0-466f-8add-f29b2278af6a" private var fetchCancellable: AnyCancellable? private var switchCancellable: AnyCancellable? required init(_ url: URL) { self.baseURL = url } func connect(_ completion: @escaping (Error?) -> Void) { // RESTful, assume we're connected unless we get an error connectionStatus = .connected completion(nil) } func disconnect(_ completion: (Error?) -> Void) { connectionStatus = .disconnected } func fetchDevices(_ fetchCompletion: @escaping (Result<[AnyDevice], Error>) -> Void) { let url = baseURL.appendingPathComponent("devices/all").authenticatedURLWithAccessToken(accessToken) self.fetchCancellable = URLSession.shared.dataTaskPublisher(for: url) .mapError { $0 as Error } .map { $0.data } .decode(type: [HubitatDevice].self, decoder: JSONDecoder()) .sink(receiveCompletion: { completion in if case let Subscribers.Completion.failure(error) = completion { self.connectionStatus = .error fetchCompletion(.failure(error)) } else { self.connectionStatus = .connected } }) { (devices: [HubitatDevice]) in self.devices = devices fetchCompletion(.success(devices.map { AnyDevice($0) })) } } func toggleDevice(_ device: AnyDevice, state: DeviceState, completion toggleCompletion: @escaping (Error?) -> Void) { if let hubitatDevice = (self.devices.first { $0.serial == device.serial }) { let command = "devices/\(hubitatDevice.id)/\(state)" let url = baseURL.appendingPathComponent(command).authenticatedURLWithAccessToken(accessToken) self.switchCancellable = URLSession.shared.dataTaskPublisher(for: url) .tryMap { data, response -> Data in guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { throw HubitatServerError.apiError } return data } .decode(type: HubitatDevice.self, decoder: JSONDecoder()) .sink(receiveCompletion: { (completion: Subscribers.Completion) in if case let Subscribers.Completion.failure(error) = completion { toggleCompletion(error) } }, receiveValue: { (device: HubitatDevice) in // don't need to do anything with this, but the server returns it to us. toggleCompletion(nil) }) } } func responsibleForDevice(_ device: AnyDevice) -> Bool { return self.devices.contains { $0.serial == device.serial } } } extension URL { func authenticatedURLWithAccessToken(_ token: String) -> URL { guard var components = URLComponents(url: self, resolvingAgainstBaseURL: false) else { fatalError() } components.queryItems = [ URLQueryItem(name: "access_token", value: token) ] return components.url! } }