ServerMultiplex: Introduces a server mutiplexer

- Also moves all the existing Wemo* stuff over to support the multiplexer
- Moves MainViewController to be the owner of the multiplexer, and its delegate
This commit is contained in:
2020-03-14 18:33:02 -07:00
parent f1f86ced29
commit 918818cd99
8 changed files with 335 additions and 83 deletions
@@ -0,0 +1,136 @@
//
// ServerMultiplex.swift
// XIONControlPanel
//
// Created by James Magahern on 3/14/20.
// Copyright © 2020 XION. All rights reserved.
//
import Foundation
protocol ServerMultiplexDelegate
{
func serverMultiplex(_ multiplex: ServerMultiplex, didAddDevices devices: [AnyDevice])
func serverMultiplex(_ multiplex: ServerMultiplex, devicesStateChanged devices: [AnyDevice])
func serverMultiplex(_ multiplex: ServerMultiplex, didEncounterError error: Error)
}
enum ServerMultiplexError : Error
{
case UnknownDevice
}
class ServerMultiplex
{
public var delegate: ServerMultiplexDelegate?
public private(set) var devices = Set<AnyDevice>()
private var servers: [Server] = []
public var numServers: Int { get { return servers.count } }
public func addServer(_ server: Server)
{
servers.append(server)
}
public func numberOfConnectedServers() -> Int
{
return servers.filter { $0.connected == true }.count
}
public func toggleDeviceState(_ device: AnyDevice, state: DeviceState, completion: @escaping (Error?) -> Void)
{
if let server = (servers.first { $0.responsibleForDevice(device) }) {
server.toggleDevice(device, state: state, completion: completion)
// Update internal state
if let index = devices.firstIndex(of: device) {
devices[index].state = state
}
} else {
completion(ServerMultiplexError.UnknownDevice)
}
}
public func refreshDevices()
{
ensureConnection { (error: Error?) in
if let error = error {
self.handleError(error)
} else {
self.servers.forEach { (server: Server) in
server.fetchDevices { (result: Result<[AnyDevice], Error>) in
self.handleServerFetchResult(result)
}
}
}
}
}
}
extension ServerMultiplex
{
private func ensureConnection(then: @escaping (Error?) -> Void)
{
let disconnectedServers = servers.filter { $0.connected == false }
var disconnectedServerCount = disconnectedServers.count
if disconnectedServerCount > 0 {
disconnectedServers.forEach { (server: Server) in
server.connect { (error: Error?) in
disconnectedServerCount -= 1
if disconnectedServerCount == 0 {
then(error)
}
}
}
} else {
then(nil)
}
}
private func handleServerFetchResult(_ result: Result<[AnyDevice], Error>)
{
switch result {
case .success(let devices):
handleDevicesChanged(devices)
case .failure(let error):
handleError(error)
}
}
private func handleDevicesChanged(_ devicesChanged: [AnyDevice])
{
let newDevicesSet = Set<AnyDevice>(devicesChanged)
let additions = newDevicesSet.subtracting(self.devices)
if additions.count > 0 {
DispatchQueue.main.async {
self.delegate?.serverMultiplex(self, didAddDevices: Array(additions))
}
}
let changed = newDevicesSet.filter { (device: AnyDevice) in
if let existing = (devices.first { $0.hashValue == device.hashValue }) {
return existing.state != device.state
}
return false
}
if changed.count > 0 {
DispatchQueue.main.async {
self.delegate?.serverMultiplex(self, devicesStateChanged: Array(changed))
}
}
self.devices = self.devices.union(newDevicesSet)
}
private func handleError(_ error: Error)
{
DispatchQueue.main.async {
self.delegate?.serverMultiplex(self, didEncounterError: error)
}
}
}
@@ -7,3 +7,27 @@
//
import Foundation
protocol Server
{
var connected: Bool { get }
/// Designated initializer. Takes an API endpoint URL
init(_ url: URL)
/// Attempts to connect to the server at the specified endpoint.
func connect(_ completion: @escaping (Error?) -> Void)
/// Sets server's state as "disconnected"
func disconnect(_ completion: (Error?) -> Void)
/// Fetches a list of all currently configured devices for this server, or returns an error.
func fetchDevices(_ completion: @escaping (Result<[AnyDevice], Error>) -> Void)
/// Tells the server to set the state of a given device to the given state
func toggleDevice(_ device: AnyDevice, state: DeviceState, completion: @escaping (Error?) -> Void)
/// Returns true if this is a device this server is responsible for
func responsibleForDevice(_ device: AnyDevice) -> Bool
}
+26 -11
View File
@@ -22,8 +22,10 @@ enum ConnectionError : Error
case serverUnavailable
}
class WemoServer
class WemoServer : Server
{
private var devices: [WemoDevice] = []
fileprivate(set) var baseURL: URL
fileprivate(set) var connected: Bool = false
@@ -31,7 +33,7 @@ class WemoServer
fileprivate var _errorStream: StandardErrorOutputStream = StandardErrorOutputStream()
fileprivate var _operationQueue: OperationQueue = OperationQueue()
init(_ url: URL)
required init(_ url: URL)
{
self.baseURL = url
@@ -68,29 +70,32 @@ class WemoServer
completion(nil)
}
func fetchDevices(_ completion: @escaping ([WemoDevice], Error?) -> Void)
func fetchDevices(_ completion: @escaping (Result<[AnyDevice], Error>) -> Void)
{
if (self.connected) {
let op = FetchDevicesOperation(baseURL: self.baseURL, session: _urlSession)
weak var weakOp = op
op.completionBlock = {
guard let strongOp = weakOp else { completion([], nil) ; return }
guard let strongOp = weakOp else { return }
if let error = strongOp.error {
self._logError("Error fetching devices", error: error)
completion(.failure(error))
} else {
self.devices = strongOp.devices
}
completion(strongOp.devices, strongOp.error)
self.devices = strongOp.devices
completion(.success(self.devices.map { AnyDevice($0) }))
}
_operationQueue.addOperation(op)
} else {
let err = ConnectionError.serverUnavailable
completion([], err)
completion(.failure(ConnectionError.serverUnavailable))
}
}
func toggleDevice(_ device: WemoDevice, state: WemoDevice.State, completion: @escaping (Error?) -> Void)
func toggleDevice(_ device: AnyDevice, state: DeviceState, completion: @escaping (Error?) -> Void)
{
if (self.connected) {
if self.connected, let device = findDevice(device) {
let op = ToggleDeviceOperation(baseURL: self.baseURL, session: _urlSession, device: device, state: state)
weak var weakOp = op
op.completionBlock = {
@@ -108,12 +113,22 @@ class WemoServer
}
}
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
@@ -188,9 +203,9 @@ internal class FetchDevicesOperation : WemoOperation
internal class ToggleDeviceOperation : WemoOperation
{
var device: WemoDevice
var state: WemoDevice.State
var state: DeviceState
init(baseURL: URL, session: URLSession, device: WemoDevice, state: WemoDevice.State)
init(baseURL: URL, session: URLSession, device: WemoDevice, state: DeviceState)
{
self.device = device
self.state = state