This fixes the problem where either internet connectivity issues on the device or intermittant server issues can cause a persistent "OFFLINE" indicator even though that is not the case. This requires the server protocol to be more granular about what their connection status is, and adds a new signal from the multiplexer to the MainViewController allowing it to notify the delegate that an acknowledgement was heard from one of the servers, allowing the main view controller to update its UI according to whatever status best represents the group of servers as a whole. Testing - This can be tested by using the network link conditioner to simulate connection issues *after* the control panel has connected to all servers in the multiplex. - Tested losing internet connection and regaining internet connection - Tested starting off with no connection and regaining connection later
108 lines
3.7 KiB
Swift
108 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
|
|
|
|
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!
|
|
}
|
|
}
|