Files
Xaibatsu-Control-Panel/XIONControlPanel/Servers/HubitatServer.swift
2025-06-19 20:43:05 -07:00

109 lines
3.7 KiB
Swift

//
// 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
weak var delegate: ServerDelegate?
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<Error>) 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!
}
}