Files
Xaibatsu-Control-Panel/XIONControlPanel/Servers/WemoServer.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

231 lines
7.0 KiB
Swift

//
// WemoServer.swift
// XIONControlPanel
//
// Created by Charles Magahern on 12/31/15.
// Copyright © 2015 XION. All rights reserved.
//
import Foundation
enum ConnectionError : Error
{
case unknown
case serverUnavailable
}
class WemoServer : Server
{
private var devices: [WemoDevice] = []
public var connectionStatus: ConnectionStatus = .disconnected
fileprivate(set) var baseURL: URL
fileprivate var _urlSession: URLSession
fileprivate var _errorStream: StandardErrorOutputStream = StandardErrorOutputStream()
fileprivate var _operationQueue: OperationQueue = OperationQueue()
required init(_ url: URL)
{
self.baseURL = url
let config = URLSessionConfiguration.default
_urlSession = URLSession(configuration: config)
_operationQueue.maxConcurrentOperationCount = 1
}
func connect(_ completion: @escaping (Error?) -> Void)
{
if (self.connectionStatus == .disconnected) {
self.connectionStatus = .connecting
let op = ConnectOperation(baseURL: self.baseURL, session: _urlSession)
weak var weakOp = op
op.completionBlock = {
guard let strongOp = weakOp else { completion(nil) ; return }
if let error = strongOp.error {
self._logError("Error connecting to server", error: error)
self.connectionStatus = .disconnected
} else {
self.connectionStatus = .connected
}
completion(strongOp.error)
}
_operationQueue.addOperation(op)
} else {
completion(nil)
}
}
func disconnect(_ completion: (Error?) -> Void)
{
self.connectionStatus = .disconnected
completion(nil)
}
func fetchDevices(_ completion: @escaping (Result<[AnyDevice], Error>) -> Void)
{
let op = FetchDevicesOperation(baseURL: self.baseURL, session: _urlSession)
op.completionBlock = { [unowned op] in
if let error = op.error {
self._logError("Error fetching devices", error: error)
completion(.failure(error))
} else {
self.devices = op.devices
}
self.devices = op.devices
completion(.success(self.devices.map { AnyDevice($0) }))
}
if self.connectionStatus == .disconnected {
connect { error in
if error == nil {
self._operationQueue.addOperation(op)
}
}
} else if self.connectionStatus == .connected {
_operationQueue.addOperation(op)
} else {
completion(.failure(ConnectionError.serverUnavailable))
}
}
func toggleDevice(_ device: AnyDevice, state: DeviceState, completion: @escaping (Error?) -> Void)
{
if connectionStatus == .connected, let device = findDevice(device) {
let op = ToggleDeviceOperation(baseURL: self.baseURL, session: _urlSession, device: device, state: state)
weak var weakOp = op
op.completionBlock = {
guard let strongOp = weakOp else { completion(nil) ; return }
if let error = strongOp.error {
self._logError("Error toggling device", error: error)
}
completion(strongOp.error)
}
_operationQueue.addOperation(op)
} else {
let err = ConnectionError.serverUnavailable
completion(err)
}
}
func responsibleForDevice(_ device: AnyDevice) -> Bool
{
return self.devices.contains { $0.serial == device.serial }
}
// MARK: Internal
internal func _logError(_ description: String, error: Error)
{
print("ERROR: \(description) \(error)", to: &_errorStream)
}
internal func findDevice(_ device: AnyDevice) -> WemoDevice?
{
return self.devices.first { $0.serial == device.serial }
}
}
internal class WemoOperation : Operation
{
var baseURL: URL
var session: URLSession
fileprivate(set) var error: Error?
init(baseURL: URL, session: URLSession)
{
self.baseURL = baseURL
self.session = session
}
}
internal class ConnectOperation : WemoOperation
{
override func main()
{
let semaphore = Semaphore(value: 0)
let url = self.baseURL.appendingPathComponent("api/environment")
var request = URLRequest(url: url)
request.httpMethod = "POST"
let task = self.session.dataTask(with: request, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) -> Void in
self.error = error
semaphore.signal()
})
task.resume()
semaphore.wait()
}
}
internal class FetchDevicesOperation : WemoOperation
{
private(set) var devices: [WemoDevice] = []
override func main()
{
let semaphore = Semaphore(value: 0)
let url = self.baseURL.appendingPathComponent("api/environment")
let task = self.session.dataTask(with: url, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) -> Void in
if (data != nil) {
self.devices = self._parseDevices(data!)
} else {
self.error = error
}
semaphore.signal()
})
task.resume()
semaphore.wait()
}
internal func _parseDevices(_ data: Data) -> [WemoDevice]
{
var devices: [WemoDevice] = []
if let responseDict = try? JSONSerialization.jsonObject(with: data, options: []) as? NSDictionary {
for responseObj in responseDict.allValues {
if let deviceDict = responseObj as? NSDictionary {
let device = WemoDevice(deviceDict)
devices.append(device)
}
}
}
return devices
}
}
internal class ToggleDeviceOperation : WemoOperation
{
var device: WemoDevice
var state: DeviceState
init(baseURL: URL, session: URLSession, device: WemoDevice, state: DeviceState)
{
self.device = device
self.state = state
super.init(baseURL: baseURL, session: session)
}
override func main()
{
let semaphore = Semaphore(value: 0)
let stateArg = (self.state == .on ? "on" : "off")
let url = self.baseURL.appendingPathComponent("api/device/\(self.device.name)").URLByAppendingRequestParameters(["state" : stateArg])
var request = URLRequest(url: url!)
request.httpMethod = "POST"
let task = self.session.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
self.error = error
semaphore.signal()
}
task.resume()
semaphore.wait()
}
}