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

View File

@@ -36,6 +36,9 @@
0CAA327D1C361ECA00B353CF /* StandardError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAA327C1C361ECA00B353CF /* StandardError.swift */; }; 0CAA327D1C361ECA00B353CF /* StandardError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAA327C1C361ECA00B353CF /* StandardError.swift */; };
0CF0F8961C362D8F00FE7752 /* NSURLAdditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF0F8951C362D8F00FE7752 /* NSURLAdditions.swift */; }; 0CF0F8961C362D8F00FE7752 /* NSURLAdditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF0F8951C362D8F00FE7752 /* NSURLAdditions.swift */; };
0CF9A48E1C35E9C800276F13 /* SwitchIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF9A48D1C35E9C800276F13 /* SwitchIndicatorView.swift */; }; 0CF9A48E1C35E9C800276F13 /* SwitchIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF9A48D1C35E9C800276F13 /* SwitchIndicatorView.swift */; };
CD815B8E241C6F4000E2FA97 /* ServerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD815B8D241C6F4000E2FA97 /* ServerProtocol.swift */; };
CD815B90241C7CAC00E2FA97 /* DeviceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD815B8F241C7CAC00E2FA97 /* DeviceProtocol.swift */; };
CD815B92241DA28B00E2FA97 /* ServerMultiplex.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD815B91241DA28B00E2FA97 /* ServerMultiplex.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
@@ -73,6 +76,9 @@
0CAA327C1C361ECA00B353CF /* StandardError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StandardError.swift; sourceTree = "<group>"; }; 0CAA327C1C361ECA00B353CF /* StandardError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StandardError.swift; sourceTree = "<group>"; };
0CF0F8951C362D8F00FE7752 /* NSURLAdditions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSURLAdditions.swift; sourceTree = "<group>"; }; 0CF0F8951C362D8F00FE7752 /* NSURLAdditions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSURLAdditions.swift; sourceTree = "<group>"; };
0CF9A48D1C35E9C800276F13 /* SwitchIndicatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwitchIndicatorView.swift; sourceTree = "<group>"; }; 0CF9A48D1C35E9C800276F13 /* SwitchIndicatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwitchIndicatorView.swift; sourceTree = "<group>"; };
CD815B8D241C6F4000E2FA97 /* ServerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerProtocol.swift; sourceTree = "<group>"; };
CD815B8F241C7CAC00E2FA97 /* DeviceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceProtocol.swift; sourceTree = "<group>"; };
CD815B91241DA28B00E2FA97 /* ServerMultiplex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerMultiplex.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@@ -112,7 +118,6 @@
0C20E2BA1C33529700BA789B /* MainViewController.swift */, 0C20E2BA1C33529700BA789B /* MainViewController.swift */,
0C41EF491C34B51500AF847F /* SwitchesViewController.swift */, 0C41EF491C34B51500AF847F /* SwitchesViewController.swift */,
0C20E2BC1C33534F00BA789B /* VisualizationViewController.swift */, 0C20E2BC1C33534F00BA789B /* VisualizationViewController.swift */,
0CAA32781C361A2400B353CF /* WemoServer.swift */,
); );
path = Controllers; path = Controllers;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -120,6 +125,7 @@
0C20E2B71C33514100BA789B /* Models */ = { 0C20E2B71C33514100BA789B /* Models */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
CD815B8F241C7CAC00E2FA97 /* DeviceProtocol.swift */,
0C4314FF1C35B1E700164559 /* WemoDevice.swift */, 0C4314FF1C35B1E700164559 /* WemoDevice.swift */,
); );
path = Models; path = Models;
@@ -169,6 +175,7 @@
0C20E2B61C33514100BA789B /* Controllers */, 0C20E2B61C33514100BA789B /* Controllers */,
0C41EF4B1C34B81F00AF847F /* External */, 0C41EF4B1C34B81F00AF847F /* External */,
0C20E2B71C33514100BA789B /* Models */, 0C20E2B71C33514100BA789B /* Models */,
CD815B8C241C6F2C00E2FA97 /* Servers */,
0C63A8741C3373BC007E4B52 /* Shaders */, 0C63A8741C3373BC007E4B52 /* Shaders */,
0C20E2C81C336F2E00BA789B /* Resources */, 0C20E2C81C336F2E00BA789B /* Resources */,
0C63A8791C33C622007E4B52 /* Utilities */, 0C63A8791C33C622007E4B52 /* Utilities */,
@@ -215,6 +222,16 @@
path = Utilities; path = Utilities;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
CD815B8C241C6F2C00E2FA97 /* Servers */ = {
isa = PBXGroup;
children = (
CD815B91241DA28B00E2FA97 /* ServerMultiplex.swift */,
CD815B8D241C6F4000E2FA97 /* ServerProtocol.swift */,
0CAA32781C361A2400B353CF /* WemoServer.swift */,
);
path = Servers;
sourceTree = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
@@ -305,6 +322,7 @@
0C41EF561C35ABCD00AF847F /* ConnectionStatusView.swift in Sources */, 0C41EF561C35ABCD00AF847F /* ConnectionStatusView.swift in Sources */,
0C63A87D1C33C655007E4B52 /* NSValue+XIONAdditions.m in Sources */, 0C63A87D1C33C655007E4B52 /* NSValue+XIONAdditions.m in Sources */,
0C20E2BD1C33534F00BA789B /* VisualizationViewController.swift in Sources */, 0C20E2BD1C33534F00BA789B /* VisualizationViewController.swift in Sources */,
CD815B90241C7CAC00E2FA97 /* DeviceProtocol.swift in Sources */,
0C41EF4A1C34B51500AF847F /* SwitchesViewController.swift in Sources */, 0C41EF4A1C34B51500AF847F /* SwitchesViewController.swift in Sources */,
0C20E2B91C33514100BA789B /* AppDelegate.swift in Sources */, 0C20E2B91C33514100BA789B /* AppDelegate.swift in Sources */,
0C20E2BB1C33529700BA789B /* MainViewController.swift in Sources */, 0C20E2BB1C33529700BA789B /* MainViewController.swift in Sources */,
@@ -312,6 +330,8 @@
0C21329B1C4DDC3E001E8F08 /* Semaphore.swift in Sources */, 0C21329B1C4DDC3E001E8F08 /* Semaphore.swift in Sources */,
0C41EF541C34BC9800AF847F /* XIONLogoView.swift in Sources */, 0C41EF541C34BC9800AF847F /* XIONLogoView.swift in Sources */,
0C4315061C35E67600164559 /* WemoCellView.swift in Sources */, 0C4315061C35E67600164559 /* WemoCellView.swift in Sources */,
CD815B92241DA28B00E2FA97 /* ServerMultiplex.swift in Sources */,
CD815B8E241C6F4000E2FA97 /* ServerProtocol.swift in Sources */,
0CAA327D1C361ECA00B353CF /* StandardError.swift in Sources */, 0CAA327D1C361ECA00B353CF /* StandardError.swift in Sources */,
0CF9A48E1C35E9C800276F13 /* SwitchIndicatorView.swift in Sources */, 0CF9A48E1C35E9C800276F13 /* SwitchIndicatorView.swift in Sources */,
0C4315001C35B1E700164559 /* WemoDevice.swift in Sources */, 0C4315001C35B1E700164559 /* WemoDevice.swift in Sources */,

View File

@@ -8,9 +8,9 @@
import UIKit import UIKit
class MainViewController: UIViewController, SwitchesViewControllerDelegate class MainViewController: UIViewController, SwitchesViewControllerDelegate, ServerMultiplexDelegate
{ {
fileprivate var _server: WemoServer fileprivate var _serverMultiplex: ServerMultiplex = ServerMultiplex()
fileprivate var _visualizationController: VisualizationViewController = VisualizationViewController() fileprivate var _visualizationController: VisualizationViewController = VisualizationViewController()
fileprivate var _switchesController: SwitchesViewController = SwitchesViewController() fileprivate var _switchesController: SwitchesViewController = SwitchesViewController()
fileprivate var _headerView: HeaderView = HeaderView() fileprivate var _headerView: HeaderView = HeaderView()
@@ -18,10 +18,10 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?)
{ {
let url = URL(string: "http://midna.xionsf.com:5000")!
_server = WemoServer(url)
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
_serverMultiplex.delegate = self
_serverMultiplex.addServer(WemoServer(URL(string: "http://midna.xionsf.com:5000")!))
} }
required init?(coder aDecoder: NSCoder) required init?(coder aDecoder: NSCoder)
@@ -104,16 +104,11 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate
_headerView.xionLogoView.beginAnimating() _headerView.xionLogoView.beginAnimating()
if (!_server.connected) { if _serverMultiplex.devices.count == 0 {
// Do initial refresh
_updateConnectivityStatus(.connecting) _updateConnectivityStatus(.connecting)
_server.connect { (error: Error?) -> Void in _serverMultiplex.refreshDevices()
if (error == nil) { _startUpdatingDevices()
self._reloadDevices()
self._startUpdatingDevices()
} else {
self._updateConnectivityStatus(.error)
}
}
} }
} }
@@ -134,25 +129,14 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate
return true return true
} }
// MARK: API
var devices: [WemoDevice] = []
{
didSet
{
_switchesController.devices = devices
_updateVisualization(false)
}
}
// MARK: SwitchesViewControllerDelegate // MARK: SwitchesViewControllerDelegate
func switchesViewControllerDidToggleDevices(_ controller: SwitchesViewController, devices: [WemoDevice]) func switchesViewControllerDidToggleDevices(_ controller: SwitchesViewController, devices: [AnyDevice])
{ {
_updateVisualization(true) _updateVisualization(true)
for device in devices { for device in devices {
_server.toggleDevice(device, state: device.state, completion: { (error: Error?) -> Void in }) _serverMultiplex.toggleDeviceState(device, state: device.state, completion: { (error: Error?) -> Void in })
} }
} }
@@ -179,14 +163,10 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate
internal func _updateVisualization(_ animated: Bool) internal func _updateVisualization(_ animated: Bool)
{ {
var activatedDevicesCount = 0 let activatedDevicesCount = _serverMultiplex.devices.filter { $0.state == .on }.count
for device in self.devices { let totalDeviceCount = _serverMultiplex.devices.count
if (device.state == .on) {
activatedDevicesCount += 1
}
}
let percentageActivated = (self.devices.count > 0 ? Float(activatedDevicesCount) / Float(self.devices.count) : 0.0) let percentageActivated = (totalDeviceCount > 0 ? Float(activatedDevicesCount) / Float(totalDeviceCount) : 0.0)
if (percentageActivated != _visualizationController.percentActivated) { if (percentageActivated != _visualizationController.percentActivated) {
_visualizationController.setPercentActivated(percentageActivated, animated: animated) _visualizationController.setPercentActivated(percentageActivated, animated: animated)
} }
@@ -201,21 +181,6 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate
} }
} }
internal func _reloadDevices()
{
_server.fetchDevices({ (devices: [WemoDevice], error: Error?) -> Void in
DispatchQueue.main.async { () -> Void in
if (error == nil) {
self.devices = devices
self._updateConnectivityStatus(.connected)
} else {
self.devices = []
self._updateConnectivityStatus(.error)
}
}
})
}
internal func _startUpdatingDevices() internal func _startUpdatingDevices()
{ {
_updateDevices = true _updateDevices = true
@@ -223,7 +188,7 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate
let interval = DispatchTime.now() + Double(Int64(10 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC) let interval = DispatchTime.now() + Double(Int64(10 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)
DispatchQueue.main.asyncAfter(deadline: interval) { () -> Void in DispatchQueue.main.asyncAfter(deadline: interval) { () -> Void in
if (self._updateDevices) { if (self._updateDevices) {
self._reloadDevices() self._serverMultiplex.refreshDevices()
self._startUpdatingDevices() self._startUpdatingDevices()
} }
} }
@@ -233,4 +198,28 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate
{ {
_updateDevices = false _updateDevices = false
} }
// MARK: Server Multiplex Delegate
func serverMultiplex(_ multiplex: ServerMultiplex, didAddDevices devices: [AnyDevice])
{
_switchesController.devices = devices
_updateVisualization(false)
// Update connection status too, if applicable
if _serverMultiplex.numServers == _serverMultiplex.numberOfConnectedServers() {
_updateConnectivityStatus(.connected)
}
}
func serverMultiplex(_ multiplex: ServerMultiplex, devicesStateChanged devices: [AnyDevice])
{
_switchesController.devicesStateChanged(devices)
}
func serverMultiplex(_ multiplex: ServerMultiplex, didEncounterError error: Error)
{
_updateConnectivityStatus(.error)
print(error.localizedDescription)
}
} }

View File

@@ -12,12 +12,7 @@ import UIKit
protocol SwitchesViewControllerDelegate: class protocol SwitchesViewControllerDelegate: class
{ {
func switchesViewControllerDidToggleDevices(_ controller: SwitchesViewController, devices: [WemoDevice]) func switchesViewControllerDidToggleDevices(_ controller: SwitchesViewController, devices: [AnyDevice])
}
extension SwitchesViewControllerDelegate
{
func switchesViewControllerDidToggleDevices(_ controller: SwitchesViewController, devices: [WemoDevice]) {}
} }
class SwitchesViewController: UIViewController, class SwitchesViewController: UIViewController,
@@ -117,12 +112,12 @@ class SwitchesViewController: UIViewController,
// MARK: API // MARK: API
var devices: [WemoDevice] = [] var devices: [AnyDevice] = []
{ {
didSet didSet
{ {
// sort devices by name // sort devices by name
self.devices.sort(by: { (d1: WemoDevice, d2: WemoDevice) -> Bool in self.devices.sort(by: { (d1: Device, d2: Device) -> Bool in
return (d1.name.compare(d2.name, options: .caseInsensitive, range: nil, locale: nil) == .orderedAscending) return (d1.name.compare(d2.name, options: .caseInsensitive, range: nil, locale: nil) == .orderedAscending)
}) })
@@ -132,6 +127,19 @@ class SwitchesViewController: UIViewController,
} }
} }
public func devicesStateChanged(_ changedDevices: [AnyDevice])
{
var snapshot = _collectionViewDataSource.snapshot()
changedDevices.forEach { changedDevice in
if let existingDevice = (self.devices.first { $0.hashValue == changedDevice.hashValue }) {
existingDevice.state = changedDevice.state
snapshot.reloadItems([ existingDevice.hashValue ])
}
}
_collectionViewDataSource.apply(snapshot, animatingDifferences: true)
}
// MARK: UICollectionView // MARK: UICollectionView
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath, identifier: Int) -> UICollectionViewCell? func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath, identifier: Int) -> UICollectionViewCell?

View File

@@ -0,0 +1,71 @@
//
// DeviceProtocol.swift
// XIONControlPanel
//
// Created by James Magahern on 3/13/20.
// Copyright © 2020 XION. All rights reserved.
//
import Foundation
enum DeviceState
{
case off
case on
}
enum DeviceType
{
case `switch`
}
protocol Device
{
// Read-only properties
var name: String { get }
var serial: String { get }
var type: DeviceType { get }
// Writable properties
var state: DeviceState { get set }
}
extension Device where Self: Hashable
{
func hash(into hasher: inout Hasher)
{
hasher.combine(self.name)
hasher.combine(self.type)
hasher.combine(self.serial)
}
static func == (lhs: Self, rhs: Self) -> Bool
{
return lhs.name == rhs.name && lhs.serial == rhs.serial
}
}
class AnyDevice : Device
{
private var device: Device
public var name: String
{ get { return device.name } }
public var serial: String
{ get { return device.serial } }
public var type: DeviceType
{ get { return device.type } }
public var state: DeviceState {
get { return device.state }
set { device.state = newValue }
}
init(_ device: Device) {
self.device = device
}
}
extension AnyDevice : Hashable {}

View File

@@ -8,23 +8,12 @@
import Foundation import Foundation
class WemoDevice: Hashable class WemoDevice : Device
{ {
enum State
{
case off
case on
}
enum DeviceType
{
case `switch`
}
var name: String = "" var name: String = ""
var host: String = "" var host: String = ""
var model: String = "" var model: String = ""
var state: State = .off var state: DeviceState = .off
var type: DeviceType = .switch var type: DeviceType = .switch
var serial: String = "" var serial: String = ""

View File

@@ -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)
}
}
}

View File

@@ -7,3 +7,27 @@
// //
import Foundation 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
}

View File

@@ -22,8 +22,10 @@ enum ConnectionError : Error
case serverUnavailable case serverUnavailable
} }
class WemoServer class WemoServer : Server
{ {
private var devices: [WemoDevice] = []
fileprivate(set) var baseURL: URL fileprivate(set) var baseURL: URL
fileprivate(set) var connected: Bool = false fileprivate(set) var connected: Bool = false
@@ -31,7 +33,7 @@ class WemoServer
fileprivate var _errorStream: StandardErrorOutputStream = StandardErrorOutputStream() fileprivate var _errorStream: StandardErrorOutputStream = StandardErrorOutputStream()
fileprivate var _operationQueue: OperationQueue = OperationQueue() fileprivate var _operationQueue: OperationQueue = OperationQueue()
init(_ url: URL) required init(_ url: URL)
{ {
self.baseURL = url self.baseURL = url
@@ -68,29 +70,32 @@ class WemoServer
completion(nil) completion(nil)
} }
func fetchDevices(_ completion: @escaping ([WemoDevice], Error?) -> Void) func fetchDevices(_ completion: @escaping (Result<[AnyDevice], Error>) -> Void)
{ {
if (self.connected) { if (self.connected) {
let op = FetchDevicesOperation(baseURL: self.baseURL, session: _urlSession) let op = FetchDevicesOperation(baseURL: self.baseURL, session: _urlSession)
weak var weakOp = op weak var weakOp = op
op.completionBlock = { op.completionBlock = {
guard let strongOp = weakOp else { completion([], nil) ; return } guard let strongOp = weakOp else { return }
if let error = strongOp.error { if let error = strongOp.error {
self._logError("Error fetching devices", error: 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) _operationQueue.addOperation(op)
} else { } else {
let err = ConnectionError.serverUnavailable completion(.failure(ConnectionError.serverUnavailable))
completion([], err)
} }
} }
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) let op = ToggleDeviceOperation(baseURL: self.baseURL, session: _urlSession, device: device, state: state)
weak var weakOp = op weak var weakOp = op
op.completionBlock = { op.completionBlock = {
@@ -108,12 +113,22 @@ class WemoServer
} }
} }
func responsibleForDevice(_ device: AnyDevice) -> Bool
{
return self.devices.contains { $0.serial == device.serial }
}
// MARK: Internal // MARK: Internal
internal func _logError(_ description: String, error: Error) internal func _logError(_ description: String, error: Error)
{ {
print("ERROR: \(description) \(error)", to: &_errorStream) print("ERROR: \(description) \(error)", to: &_errorStream)
} }
internal func findDevice(_ device: AnyDevice) -> WemoDevice?
{
return self.devices.first { $0.serial == device.serial }
}
} }
internal class WemoOperation : Operation internal class WemoOperation : Operation
@@ -188,9 +203,9 @@ internal class FetchDevicesOperation : WemoOperation
internal class ToggleDeviceOperation : WemoOperation internal class ToggleDeviceOperation : WemoOperation
{ {
var device: WemoDevice 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.device = device
self.state = state self.state = state