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
231 lines
7.0 KiB
Swift
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()
|
|
}
|
|
}
|