Merge branch 'buzzert/offline_fix' into 'master'

Fixes issue where connectivity issues can lead to persistent "Offline" status in UI

See merge request zanneth/XIONControlPanel!2
This commit is contained in:
zanneth
2020-07-04 22:22:39 -04:00
5 changed files with 55 additions and 31 deletions

View File

@@ -206,11 +206,6 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate, Serv
{ {
_switchesController.devices = Array(multiplex.devices) _switchesController.devices = Array(multiplex.devices)
_updateVisualization(false) _updateVisualization(false)
// Update connection status too, if applicable
if _serverMultiplex.numServers == _serverMultiplex.numberOfConnectedServers() {
_updateConnectivityStatus(.connected)
}
} }
func serverMultiplex(_ multiplex: ServerMultiplex, devicesStateChanged devices: [AnyDevice]) func serverMultiplex(_ multiplex: ServerMultiplex, devicesStateChanged devices: [AnyDevice])
@@ -218,6 +213,13 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate, Serv
_switchesController.devicesStateChanged(devices) _switchesController.devicesStateChanged(devices)
} }
func serverMultiplex(_ multiplex: ServerMultiplex, didReceiveAcknowledgementFromServer server: Server)
{
// Update connection status too, if applicable
let groupConnectionStatus = multiplex.groupConnectionStatus()
_updateConnectivityStatus(groupConnectionStatus)
}
func serverMultiplex(_ multiplex: ServerMultiplex, didEncounterError error: Error) func serverMultiplex(_ multiplex: ServerMultiplex, didEncounterError error: Error)
{ {
_updateConnectivityStatus(.error) _updateConnectivityStatus(.error)

View File

@@ -16,7 +16,7 @@ public enum HubitatServerError : Error
public class HubitatServer : Server public class HubitatServer : Server
{ {
var connected: Bool = false var connectionStatus: ConnectionStatus = .disconnected
public fileprivate(set) var devices: [HubitatDevice] = [] public fileprivate(set) var devices: [HubitatDevice] = []
@@ -33,13 +33,13 @@ public class HubitatServer : Server
func connect(_ completion: @escaping (Error?) -> Void) func connect(_ completion: @escaping (Error?) -> Void)
{ {
// RESTful, assume we're connected unless we get an error // RESTful, assume we're connected unless we get an error
self.connected = true connectionStatus = .connected
completion(nil) completion(nil)
} }
func disconnect(_ completion: (Error?) -> Void) func disconnect(_ completion: (Error?) -> Void)
{ {
self.connected = false connectionStatus = .disconnected
} }
func fetchDevices(_ fetchCompletion: @escaping (Result<[AnyDevice], Error>) -> Void) func fetchDevices(_ fetchCompletion: @escaping (Result<[AnyDevice], Error>) -> Void)
@@ -52,10 +52,10 @@ public class HubitatServer : Server
.decode(type: [HubitatDevice].self, decoder: JSONDecoder()) .decode(type: [HubitatDevice].self, decoder: JSONDecoder())
.sink(receiveCompletion: { completion in .sink(receiveCompletion: { completion in
if case let Subscribers.Completion.failure(error) = completion { if case let Subscribers.Completion.failure(error) = completion {
self.connected = false self.connectionStatus = .error
fetchCompletion(.failure(error)) fetchCompletion(.failure(error))
} else { } else {
self.connected = true self.connectionStatus = .connected
} }
}) { (devices: [HubitatDevice]) in }) { (devices: [HubitatDevice]) in
self.devices = devices self.devices = devices

View File

@@ -12,6 +12,7 @@ protocol ServerMultiplexDelegate
{ {
func serverMultiplex(_ multiplex: ServerMultiplex, didAddDevices devices: [AnyDevice]) func serverMultiplex(_ multiplex: ServerMultiplex, didAddDevices devices: [AnyDevice])
func serverMultiplex(_ multiplex: ServerMultiplex, devicesStateChanged devices: [AnyDevice]) func serverMultiplex(_ multiplex: ServerMultiplex, devicesStateChanged devices: [AnyDevice])
func serverMultiplex(_ multiplex: ServerMultiplex, didReceiveAcknowledgementFromServer server: Server)
func serverMultiplex(_ multiplex: ServerMultiplex, didEncounterError error: Error) func serverMultiplex(_ multiplex: ServerMultiplex, didEncounterError error: Error)
} }
@@ -33,9 +34,26 @@ class ServerMultiplex
servers.append(server) servers.append(server)
} }
public func numberOfConnectedServers() -> Int /// Returns a status that best represents the status of the whole group, as if it were one device
public func groupConnectionStatus() -> ConnectionStatus
{ {
return servers.filter { $0.connected == true }.count let relevancyRank: (ConnectionStatus) -> Int = { status in
switch status {
case .disconnected: return 0
case .connected: return 1
case .connecting: return 2
case .error: return 3
}
}
var groupStatus = ConnectionStatus.connected
servers.forEach { server in
if relevancyRank(server.connectionStatus) > relevancyRank(groupStatus) {
groupStatus = server.connectionStatus
}
}
return groupStatus
} }
public func toggleDeviceState(_ device: AnyDevice, state: DeviceState, completion: @escaping (Error?) -> Void) public func toggleDeviceState(_ device: AnyDevice, state: DeviceState, completion: @escaping (Error?) -> Void)
@@ -56,7 +74,7 @@ class ServerMultiplex
{ {
self.servers.forEach { (server: Server) in self.servers.forEach { (server: Server) in
server.fetchDevices { (result: Result<[AnyDevice], Error>) in server.fetchDevices { (result: Result<[AnyDevice], Error>) in
self.handleServerFetchResult(result) self.handleServerFetchResult(forServer: server, result: result)
} }
} }
} }
@@ -64,18 +82,22 @@ class ServerMultiplex
extension ServerMultiplex extension ServerMultiplex
{ {
private func handleServerFetchResult(_ result: Result<[AnyDevice], Error>) private func handleServerFetchResult(forServer server: Server, result: Result<[AnyDevice], Error>)
{ {
switch result { switch result {
case .success(let devices): case .success(let devices):
handleDevicesChanged(devices) handleDevicesChanged(forServer: server, devicesChanged: devices)
case .failure(let error): case .failure(let error):
handleError(error) handleError(forServer: server, error: error)
} }
} }
private func handleDevicesChanged(_ devicesChanged: [AnyDevice]) private func handleDevicesChanged(forServer server: Server, devicesChanged: [AnyDevice])
{ {
// First send server acknowledgement
self.delegate?.serverMultiplex(self, didReceiveAcknowledgementFromServer: server)
// Then, optionally notify about new devices or device state changes
let newDevicesSet = Set<AnyDevice>(devicesChanged) let newDevicesSet = Set<AnyDevice>(devicesChanged)
let additions = newDevicesSet.subtracting(self.devices) let additions = newDevicesSet.subtracting(self.devices)
let changed = newDevicesSet.filter { (device: AnyDevice) in let changed = newDevicesSet.filter { (device: AnyDevice) in
@@ -101,7 +123,7 @@ extension ServerMultiplex
} }
} }
private func handleError(_ error: Error) private func handleError(forServer server: Server, error: Error)
{ {
DispatchQueue.main.async { DispatchQueue.main.async {
self.delegate?.serverMultiplex(self, didEncounterError: error) self.delegate?.serverMultiplex(self, didEncounterError: error)

View File

@@ -8,9 +8,17 @@
import Foundation import Foundation
enum ConnectionStatus
{
case disconnected
case connecting
case connected
case error
}
protocol Server protocol Server
{ {
var connected: Bool { get } var connectionStatus: ConnectionStatus { get }
/// Designated initializer. Takes an API endpoint URL /// Designated initializer. Takes an API endpoint URL
init(_ url: URL) init(_ url: URL)

View File

@@ -8,14 +8,6 @@
import Foundation import Foundation
enum ConnectionStatus
{
case disconnected
case connecting
case connected
case error
}
enum ConnectionError : Error enum ConnectionError : Error
{ {
case unknown case unknown
@@ -25,11 +17,9 @@ enum ConnectionError : Error
class WemoServer : Server class WemoServer : Server
{ {
private var devices: [WemoDevice] = [] private var devices: [WemoDevice] = []
public var connectionStatus: ConnectionStatus = .disconnected
public var connected: Bool { get { return self.connectionStatus == .connected } }
fileprivate(set) var baseURL: URL fileprivate(set) var baseURL: URL
fileprivate(set) var connectionStatus: ConnectionStatus = .disconnected
fileprivate var _urlSession: URLSession fileprivate var _urlSession: URLSession
fileprivate var _errorStream: StandardErrorOutputStream = StandardErrorOutputStream() fileprivate var _errorStream: StandardErrorOutputStream = StandardErrorOutputStream()
@@ -48,6 +38,8 @@ class WemoServer : Server
func connect(_ completion: @escaping (Error?) -> Void) func connect(_ completion: @escaping (Error?) -> Void)
{ {
if (self.connectionStatus == .disconnected) { if (self.connectionStatus == .disconnected) {
self.connectionStatus = .connecting
let op = ConnectOperation(baseURL: self.baseURL, session: _urlSession) let op = ConnectOperation(baseURL: self.baseURL, session: _urlSession)
weak var weakOp = op weak var weakOp = op
op.completionBlock = { op.completionBlock = {
@@ -103,7 +95,7 @@ class WemoServer : Server
func toggleDevice(_ device: AnyDevice, state: DeviceState, completion: @escaping (Error?) -> Void) func toggleDevice(_ device: AnyDevice, state: DeviceState, completion: @escaping (Error?) -> Void)
{ {
if self.connected, let device = findDevice(device) { if connectionStatus == .connected, let device = findDevice(device) {
let op = ToggleDeviceOperation(baseURL: self.baseURL, session: _urlSession, device: device, state: state) let op = ToggleDeviceOperation(baseURL: self.baseURL, session: _urlSession, device: device, state: state)
weak var weakOp = op weak var weakOp = op
op.completionBlock = { op.completionBlock = {