Files
Xaibatsu-Control-Panel/XIONControlPanel/Servers/HubitatServer.swift
James Magahern d1da677932 Fixes issue where connectivity issues can lead to persistent "Offline" status in UI
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
2020-07-04 19:43:50 -07:00

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!
}
}