From 5b521541febee64d82ad44a76ccff49fba16394d Mon Sep 17 00:00:00 2001 From: James Magahern Date: Fri, 13 Mar 2020 18:46:59 -0700 Subject: [PATCH 1/9] Update to Xcode's recommended settings --- XIONControlPanel.xcodeproj/project.pbxproj | 27 +++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/XIONControlPanel.xcodeproj/project.pbxproj b/XIONControlPanel.xcodeproj/project.pbxproj index 9cfefad..5bb7d3f 100644 --- a/XIONControlPanel.xcodeproj/project.pbxproj +++ b/XIONControlPanel.xcodeproj/project.pbxproj @@ -242,7 +242,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0720; - LastUpgradeCheck = 0720; + LastUpgradeCheck = 1200; ORGANIZATIONNAME = XION; TargetAttributes = { 0C3CAEA31C3350C800B856AD = { @@ -336,17 +336,29 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -380,17 +392,29 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -408,6 +432,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; From f1f86ced290ed1823a5a48135b367c77a54772e9 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Fri, 13 Mar 2020 19:36:44 -0700 Subject: [PATCH 2/9] SwitchesViewController: Modernize switches collection view controller This switches to using the more modern collection view diffable data source API. Also I moved the action cells into their own section so we don't need to do arithmetic to determine whether or not a cell is an action cell or a device cell. --- .../Controllers/SwitchesViewController.swift | 175 ++++++++---------- XIONControlPanel/Servers/ServerProtocol.swift | 9 + .../{Controllers => Servers}/WemoServer.swift | 0 3 files changed, 82 insertions(+), 102 deletions(-) create mode 100644 XIONControlPanel/Servers/ServerProtocol.swift rename XIONControlPanel/{Controllers => Servers}/WemoServer.swift (100%) diff --git a/XIONControlPanel/Controllers/SwitchesViewController.swift b/XIONControlPanel/Controllers/SwitchesViewController.swift index cd787aa..663205a 100644 --- a/XIONControlPanel/Controllers/SwitchesViewController.swift +++ b/XIONControlPanel/Controllers/SwitchesViewController.swift @@ -21,20 +21,25 @@ extension SwitchesViewControllerDelegate } class SwitchesViewController: UIViewController, - UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { - weak var delegate: SwitchesViewControllerDelegate? + weak var delegate: SwitchesViewControllerDelegate? - fileprivate var _collectionView: UICollectionView = UICollectionView(frame: CGRect.zero, - collectionViewLayout: UICollectionViewFlowLayout()) - fileprivate var _currentDevicesHash: Int = 0 + fileprivate var _collectionView: UICollectionView = UICollectionView(frame: CGRect.zero, + collectionViewLayout: UICollectionViewFlowLayout()) + + fileprivate var _collectionViewDataSource: UICollectionViewDiffableDataSource! + + fileprivate var _currentDevicesHash: Int = 0 static fileprivate let collectionViewDeviceSwitchCellReuseIdentifier = "DeviceSwitchReuseID" static fileprivate let collectionViewActionCellReuseIdentifier = "ActionCellReuseID" static fileprivate let collectionViewCellsSpacing: CGFloat = 5.0 - fileprivate enum ActionCell: Int + static fileprivate let actionCellsSectionIdentifier = 0 + static fileprivate let switchCellsSectionIdentifier = 1 + + fileprivate enum ActionCell: Int, CaseIterable { case allOn case allOff @@ -49,13 +54,37 @@ class SwitchesViewController: UIViewController, } } - static let count: Int = { - var max = 0 - while let _ = ActionCell(rawValue: max) { max += 1 } - return max - }() + static let count: Int = { return ActionCell.allCases.count }() } + override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) + { + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + + _collectionViewDataSource = UICollectionViewDiffableDataSource(collectionView: _collectionView, cellProvider: { (collectionView, indexPath, identifier) -> UICollectionViewCell? in + return self.collectionView(collectionView, cellForItemAt: indexPath, identifier: identifier) + }) + + // Initialize data source + var snapshot = _collectionViewDataSource.snapshot() + + // Sections + snapshot.appendSections([ + SwitchesViewController.actionCellsSectionIdentifier, + SwitchesViewController.switchCellsSectionIdentifier, + ]) + + // Action cells + snapshot.appendItems([ + SwitchesViewController.ActionCell.allOn.rawValue, + SwitchesViewController.ActionCell.allOff.rawValue + ], toSection: SwitchesViewController.actionCellsSectionIdentifier) + + _collectionViewDataSource.apply(snapshot, animatingDifferences: false) + } + + required init?(coder: NSCoder) { fatalError("Unimplemented") } + override func viewDidLoad() { super.viewDidLoad() @@ -69,7 +98,7 @@ class SwitchesViewController: UIViewController, _collectionView.backgroundColor = UIColor.black _collectionView.delegate = self - _collectionView.dataSource = self + _collectionView.dataSource = _collectionViewDataSource _collectionView.register(WemoDeviceCellView.self, forCellWithReuseIdentifier: deviceCellReuseID) _collectionView.register(WemoActionCellView.self, forCellWithReuseIdentifier: actionCellReuseID) self.view.addSubview(_collectionView) @@ -97,87 +126,37 @@ class SwitchesViewController: UIViewController, return (d1.name.compare(d2.name, options: .caseInsensitive, range: nil, locale: nil) == .orderedAscending) }) - let hash = self.devices.reduce(0, {$0 ^ $1.hashValue}) - if (hash != _currentDevicesHash) { - let previousSet = NSOrderedSet(array: oldValue) - let newSet = NSOrderedSet(array: self.devices) - var insertedIndexPaths: [IndexPath] = [] - var updatedIndexPaths: [IndexPath] = [] - var deletedIndexPaths: [IndexPath] = [] - - // if we have devices now and we didn't before, or vice versa, - // we need to update the action cells - if ((oldValue.count == 0 && self.devices.count != 0) || (self.devices.count == 0 && oldValue.count != 0)) { - for actionCellIdx in 0 ..< ActionCell.count { - let actionCellIndexPath = IndexPath(item: actionCellIdx, section: 0) - updatedIndexPaths.append(actionCellIndexPath) - } - } - - // find deletes and updates - for (idx, device) in previousSet.enumerated() { - let itemIndex = idx + ActionCell.count - let curIndexPath = IndexPath(item: itemIndex, section: 0) - - if (!newSet.contains(device)) { - deletedIndexPaths.append(curIndexPath) - } else if (idx < newSet.count) { - let deviceInNewSet = newSet.object(at: idx) as! WemoDevice - if (deviceInNewSet != (device as! WemoDevice)) { - updatedIndexPaths.append(curIndexPath) - } - } - } - - // find insertions - for (idx, device) in newSet.enumerated() { - if (!previousSet.contains(device)) { - let itemIndex = idx + ActionCell.count - let insertedIndexPath = IndexPath(item: itemIndex, section: 0) - insertedIndexPaths.append(insertedIndexPath) - } - } - - UIView.performWithoutAnimation { () -> Void in - self._collectionView.performBatchUpdates({ () -> Void in - self._collectionView.deleteItems(at: deletedIndexPaths) - self._collectionView.reloadItems(at: updatedIndexPaths) - self._collectionView.insertItems(at: insertedIndexPaths) - }, completion: nil) - } - - _currentDevicesHash = hash - } + var snapshot = _collectionViewDataSource.snapshot() + snapshot.appendItems(self.devices.map { $0.hashValue }, toSection: SwitchesViewController.switchCellsSectionIdentifier) + _collectionViewDataSource.apply(snapshot, animatingDifferences: false) } } // MARK: UICollectionView - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath, identifier: Int) -> UICollectionViewCell? { - return self.devices.count + ActionCell.count - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell - { - if (indexPath.item < ActionCell.count) { + if (indexPath.section == SwitchesViewController.actionCellsSectionIdentifier) { let reuseID = SwitchesViewController.collectionViewActionCellReuseIdentifier let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseID, for: indexPath) as! WemoActionCellView - cell.textLabel.text = ActionCell(rawValue: indexPath.item)?.name().uppercased() + cell.textLabel.text = ActionCell(rawValue: identifier)?.name().uppercased() cell.enabled = (self.devices.count > 0) return cell - } else { + } else if (indexPath.section == SwitchesViewController.switchCellsSectionIdentifier) { let reuseID = SwitchesViewController.collectionViewDeviceSwitchCellReuseIdentifier let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseID, for: indexPath) as! WemoDeviceCellView - let device = _deviceAtIndexPath(indexPath) - cell.deviceName = device.name - cell.toggled = (device.state == .on) - cell.ordinal = indexPath.item - ActionCell.count + 1 + if let device = (self.devices.first { $0.hashValue == identifier }) { + cell.deviceName = device.name + cell.toggled = (device.state == .on) + cell.ordinal = indexPath.row + } return cell } + + return nil } func collectionView(_ collectionView: UICollectionView, @@ -206,28 +185,33 @@ class SwitchesViewController: UIViewController, } let dimensions = floor((collectionView.bounds.size.width / cellsPerRow) - ((spacing * (cellsPerRow - 1.0)) / cellsPerRow)) - if (indexPath.item < ActionCell.count) { + if (indexPath.section == SwitchesViewController.actionCellsSectionIdentifier) { return CGSize(width: collectionView.bounds.size.width, height: rint(dimensions / 1.5)) } else { return CGSize(width: dimensions, height: dimensions) } } + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets + { + let spacing = SwitchesViewController.collectionViewCellsSpacing + return UIEdgeInsets(top: spacing, left: 0, bottom: 0, right: 0) + } + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - if (indexPath.item < ActionCell.count) { + if (indexPath.section == SwitchesViewController.actionCellsSectionIdentifier) { let tappedActionCell = ActionCell(rawValue: indexPath.item) - var currentDelay: TimeInterval = 0.0 - for i in ActionCell.count ..< collectionView.numberOfItems(inSection: indexPath.section) { - if let cell = collectionView.cellForItem(at: IndexPath(item: i, section: indexPath.section)) as? WemoDeviceCellView { + for cell in collectionView.visibleCells { + if collectionView.indexPath(for: cell)?.section == SwitchesViewController.switchCellsSectionIdentifier { + let switchCell = cell as! WemoDeviceCellView let animOptions = UIView.AnimationOptions([.allowUserInteraction]) - UIView.animate(withDuration: 0.3, delay: currentDelay, options: animOptions, animations: { - cell.toggled = (tappedActionCell == .allOn) + let delay: TimeInterval = Double(switchCell.ordinal) * 0.05 + UIView.animate(withDuration: 0.3, delay: delay, options: animOptions, animations: { + switchCell.toggled = (tappedActionCell == .allOn) }, completion: nil) - - currentDelay += 0.05 } } @@ -236,11 +220,11 @@ class SwitchesViewController: UIViewController, } self.delegate?.switchesViewControllerDidToggleDevices(self, devices: self.devices) - } else { + } else if (indexPath.section == SwitchesViewController.switchCellsSectionIdentifier) { let cell = collectionView.cellForItem(at: indexPath) as! WemoDeviceCellView cell.toggled = !cell.toggled - let device = _deviceAtIndexPath(indexPath) + let device = self.devices[indexPath.row] device.state = (cell.toggled ? .on : .off) self.delegate?.switchesViewControllerDidToggleDevices(self, devices: [device]) @@ -249,28 +233,15 @@ class SwitchesViewController: UIViewController, func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool { - if (indexPath.item < ActionCell.count) { - return (self.devices.count > 0) - } else { - return true - } + return self.collectionView(collectionView, shouldSelectItemAt: indexPath) } func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { - if (indexPath.item < ActionCell.count) { + if (indexPath.section == SwitchesViewController.actionCellsSectionIdentifier) { return (self.devices.count > 0) } else { return true } } - - // MARK: Internal - - internal func _deviceAtIndexPath(_ indexPath: IndexPath) -> WemoDevice - { - let deviceIdx = indexPath.item - ActionCell.count - let device = self.devices[deviceIdx] - return device - } } diff --git a/XIONControlPanel/Servers/ServerProtocol.swift b/XIONControlPanel/Servers/ServerProtocol.swift new file mode 100644 index 0000000..fed384a --- /dev/null +++ b/XIONControlPanel/Servers/ServerProtocol.swift @@ -0,0 +1,9 @@ +// +// ServerProtocol.swift +// XIONControlPanel +// +// Created by James Magahern on 3/13/20. +// Copyright © 2020 XION. All rights reserved. +// + +import Foundation diff --git a/XIONControlPanel/Controllers/WemoServer.swift b/XIONControlPanel/Servers/WemoServer.swift similarity index 100% rename from XIONControlPanel/Controllers/WemoServer.swift rename to XIONControlPanel/Servers/WemoServer.swift From 918818cd99fb93b3c8f58406f591340d37b4ba41 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Sat, 14 Mar 2020 18:33:02 -0700 Subject: [PATCH 3/9] 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 --- XIONControlPanel.xcodeproj/project.pbxproj | 22 ++- .../Controllers/MainViewController.swift | 89 +++++------- .../Controllers/SwitchesViewController.swift | 24 ++-- XIONControlPanel/Models/DeviceProtocol.swift | 71 +++++++++ XIONControlPanel/Models/WemoDevice.swift | 15 +- .../Servers/ServerMultiplex.swift | 136 ++++++++++++++++++ XIONControlPanel/Servers/ServerProtocol.swift | 24 ++++ XIONControlPanel/Servers/WemoServer.swift | 37 +++-- 8 files changed, 335 insertions(+), 83 deletions(-) create mode 100644 XIONControlPanel/Models/DeviceProtocol.swift create mode 100644 XIONControlPanel/Servers/ServerMultiplex.swift diff --git a/XIONControlPanel.xcodeproj/project.pbxproj b/XIONControlPanel.xcodeproj/project.pbxproj index 5bb7d3f..a2dc1ec 100644 --- a/XIONControlPanel.xcodeproj/project.pbxproj +++ b/XIONControlPanel.xcodeproj/project.pbxproj @@ -36,6 +36,9 @@ 0CAA327D1C361ECA00B353CF /* StandardError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAA327C1C361ECA00B353CF /* StandardError.swift */; }; 0CF0F8961C362D8F00FE7752 /* NSURLAdditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF0F8951C362D8F00FE7752 /* NSURLAdditions.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 */ /* Begin PBXFileReference section */ @@ -73,6 +76,9 @@ 0CAA327C1C361ECA00B353CF /* StandardError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StandardError.swift; sourceTree = ""; }; 0CF0F8951C362D8F00FE7752 /* NSURLAdditions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSURLAdditions.swift; sourceTree = ""; }; 0CF9A48D1C35E9C800276F13 /* SwitchIndicatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwitchIndicatorView.swift; sourceTree = ""; }; + CD815B8D241C6F4000E2FA97 /* ServerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerProtocol.swift; sourceTree = ""; }; + CD815B8F241C7CAC00E2FA97 /* DeviceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceProtocol.swift; sourceTree = ""; }; + CD815B91241DA28B00E2FA97 /* ServerMultiplex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerMultiplex.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -112,7 +118,6 @@ 0C20E2BA1C33529700BA789B /* MainViewController.swift */, 0C41EF491C34B51500AF847F /* SwitchesViewController.swift */, 0C20E2BC1C33534F00BA789B /* VisualizationViewController.swift */, - 0CAA32781C361A2400B353CF /* WemoServer.swift */, ); path = Controllers; sourceTree = ""; @@ -120,6 +125,7 @@ 0C20E2B71C33514100BA789B /* Models */ = { isa = PBXGroup; children = ( + CD815B8F241C7CAC00E2FA97 /* DeviceProtocol.swift */, 0C4314FF1C35B1E700164559 /* WemoDevice.swift */, ); path = Models; @@ -169,6 +175,7 @@ 0C20E2B61C33514100BA789B /* Controllers */, 0C41EF4B1C34B81F00AF847F /* External */, 0C20E2B71C33514100BA789B /* Models */, + CD815B8C241C6F2C00E2FA97 /* Servers */, 0C63A8741C3373BC007E4B52 /* Shaders */, 0C20E2C81C336F2E00BA789B /* Resources */, 0C63A8791C33C622007E4B52 /* Utilities */, @@ -215,6 +222,16 @@ path = Utilities; sourceTree = ""; }; + CD815B8C241C6F2C00E2FA97 /* Servers */ = { + isa = PBXGroup; + children = ( + CD815B91241DA28B00E2FA97 /* ServerMultiplex.swift */, + CD815B8D241C6F4000E2FA97 /* ServerProtocol.swift */, + 0CAA32781C361A2400B353CF /* WemoServer.swift */, + ); + path = Servers; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -305,6 +322,7 @@ 0C41EF561C35ABCD00AF847F /* ConnectionStatusView.swift in Sources */, 0C63A87D1C33C655007E4B52 /* NSValue+XIONAdditions.m in Sources */, 0C20E2BD1C33534F00BA789B /* VisualizationViewController.swift in Sources */, + CD815B90241C7CAC00E2FA97 /* DeviceProtocol.swift in Sources */, 0C41EF4A1C34B51500AF847F /* SwitchesViewController.swift in Sources */, 0C20E2B91C33514100BA789B /* AppDelegate.swift in Sources */, 0C20E2BB1C33529700BA789B /* MainViewController.swift in Sources */, @@ -312,6 +330,8 @@ 0C21329B1C4DDC3E001E8F08 /* Semaphore.swift in Sources */, 0C41EF541C34BC9800AF847F /* XIONLogoView.swift in Sources */, 0C4315061C35E67600164559 /* WemoCellView.swift in Sources */, + CD815B92241DA28B00E2FA97 /* ServerMultiplex.swift in Sources */, + CD815B8E241C6F4000E2FA97 /* ServerProtocol.swift in Sources */, 0CAA327D1C361ECA00B353CF /* StandardError.swift in Sources */, 0CF9A48E1C35E9C800276F13 /* SwitchIndicatorView.swift in Sources */, 0C4315001C35B1E700164559 /* WemoDevice.swift in Sources */, diff --git a/XIONControlPanel/Controllers/MainViewController.swift b/XIONControlPanel/Controllers/MainViewController.swift index 54a81e7..4c07bbb 100644 --- a/XIONControlPanel/Controllers/MainViewController.swift +++ b/XIONControlPanel/Controllers/MainViewController.swift @@ -8,9 +8,9 @@ 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 _switchesController: SwitchesViewController = SwitchesViewController() fileprivate var _headerView: HeaderView = HeaderView() @@ -18,10 +18,10 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate 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) + + _serverMultiplex.delegate = self + _serverMultiplex.addServer(WemoServer(URL(string: "http://midna.xionsf.com:5000")!)) } required init?(coder aDecoder: NSCoder) @@ -104,16 +104,11 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate _headerView.xionLogoView.beginAnimating() - if (!_server.connected) { + if _serverMultiplex.devices.count == 0 { + // Do initial refresh _updateConnectivityStatus(.connecting) - _server.connect { (error: Error?) -> Void in - if (error == nil) { - self._reloadDevices() - self._startUpdatingDevices() - } else { - self._updateConnectivityStatus(.error) - } - } + _serverMultiplex.refreshDevices() + _startUpdatingDevices() } } @@ -134,25 +129,14 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate return true } - // MARK: API - - var devices: [WemoDevice] = [] - { - didSet - { - _switchesController.devices = devices - _updateVisualization(false) - } - } - // MARK: SwitchesViewControllerDelegate - func switchesViewControllerDidToggleDevices(_ controller: SwitchesViewController, devices: [WemoDevice]) + func switchesViewControllerDidToggleDevices(_ controller: SwitchesViewController, devices: [AnyDevice]) { _updateVisualization(true) 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) { - var activatedDevicesCount = 0 - for device in self.devices { - if (device.state == .on) { - activatedDevicesCount += 1 - } - } + let activatedDevicesCount = _serverMultiplex.devices.filter { $0.state == .on }.count + let totalDeviceCount = _serverMultiplex.devices.count - 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) { _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() { _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) DispatchQueue.main.asyncAfter(deadline: interval) { () -> Void in if (self._updateDevices) { - self._reloadDevices() + self._serverMultiplex.refreshDevices() self._startUpdatingDevices() } } @@ -233,4 +198,28 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate { _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) + } } diff --git a/XIONControlPanel/Controllers/SwitchesViewController.swift b/XIONControlPanel/Controllers/SwitchesViewController.swift index 663205a..61d5e0b 100644 --- a/XIONControlPanel/Controllers/SwitchesViewController.swift +++ b/XIONControlPanel/Controllers/SwitchesViewController.swift @@ -12,12 +12,7 @@ import UIKit protocol SwitchesViewControllerDelegate: class { - func switchesViewControllerDidToggleDevices(_ controller: SwitchesViewController, devices: [WemoDevice]) -} - -extension SwitchesViewControllerDelegate -{ - func switchesViewControllerDidToggleDevices(_ controller: SwitchesViewController, devices: [WemoDevice]) {} + func switchesViewControllerDidToggleDevices(_ controller: SwitchesViewController, devices: [AnyDevice]) } class SwitchesViewController: UIViewController, @@ -117,12 +112,12 @@ class SwitchesViewController: UIViewController, // MARK: API - var devices: [WemoDevice] = [] + var devices: [AnyDevice] = [] { didSet { // 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) }) @@ -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 func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath, identifier: Int) -> UICollectionViewCell? diff --git a/XIONControlPanel/Models/DeviceProtocol.swift b/XIONControlPanel/Models/DeviceProtocol.swift new file mode 100644 index 0000000..515d655 --- /dev/null +++ b/XIONControlPanel/Models/DeviceProtocol.swift @@ -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 {} diff --git a/XIONControlPanel/Models/WemoDevice.swift b/XIONControlPanel/Models/WemoDevice.swift index e8c0849..45d7b59 100644 --- a/XIONControlPanel/Models/WemoDevice.swift +++ b/XIONControlPanel/Models/WemoDevice.swift @@ -8,23 +8,12 @@ import Foundation -class WemoDevice: Hashable +class WemoDevice : Device { - enum State - { - case off - case on - } - - enum DeviceType - { - case `switch` - } - var name: String = "" var host: String = "" var model: String = "" - var state: State = .off + var state: DeviceState = .off var type: DeviceType = .switch var serial: String = "" diff --git a/XIONControlPanel/Servers/ServerMultiplex.swift b/XIONControlPanel/Servers/ServerMultiplex.swift new file mode 100644 index 0000000..95d9874 --- /dev/null +++ b/XIONControlPanel/Servers/ServerMultiplex.swift @@ -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() + + 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(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) + } + } +} diff --git a/XIONControlPanel/Servers/ServerProtocol.swift b/XIONControlPanel/Servers/ServerProtocol.swift index fed384a..c9ab118 100644 --- a/XIONControlPanel/Servers/ServerProtocol.swift +++ b/XIONControlPanel/Servers/ServerProtocol.swift @@ -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 +} + diff --git a/XIONControlPanel/Servers/WemoServer.swift b/XIONControlPanel/Servers/WemoServer.swift index 0348a0d..a364cf6 100644 --- a/XIONControlPanel/Servers/WemoServer.swift +++ b/XIONControlPanel/Servers/WemoServer.swift @@ -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 From 9bff3dda4885d6cc25095490e83ecdda97ae5d81 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Sat, 14 Mar 2020 19:53:18 -0700 Subject: [PATCH 4/9] HubitatServer: Adds HubitatServer type and adds it to the multiplexer --- XIONControlPanel.xcodeproj/project.pbxproj | 8 ++ .../Controllers/MainViewController.swift | 3 +- .../Controllers/SwitchesViewController.swift | 1 + XIONControlPanel/Models/DeviceProtocol.swift | 10 +-- XIONControlPanel/Models/HubitatDevice.swift | 47 +++++++++++ XIONControlPanel/Servers/HubitatServer.swift | 82 +++++++++++++++++++ .../Servers/ServerMultiplex.swift | 17 ++-- 7 files changed, 153 insertions(+), 15 deletions(-) create mode 100644 XIONControlPanel/Models/HubitatDevice.swift create mode 100644 XIONControlPanel/Servers/HubitatServer.swift diff --git a/XIONControlPanel.xcodeproj/project.pbxproj b/XIONControlPanel.xcodeproj/project.pbxproj index a2dc1ec..5c2018d 100644 --- a/XIONControlPanel.xcodeproj/project.pbxproj +++ b/XIONControlPanel.xcodeproj/project.pbxproj @@ -39,6 +39,8 @@ 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 */; }; + CD815B94241DC16700E2FA97 /* HubitatServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD815B93241DC16700E2FA97 /* HubitatServer.swift */; }; + CD815B96241DC44D00E2FA97 /* HubitatDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD815B95241DC44D00E2FA97 /* HubitatDevice.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -79,6 +81,8 @@ CD815B8D241C6F4000E2FA97 /* ServerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerProtocol.swift; sourceTree = ""; }; CD815B8F241C7CAC00E2FA97 /* DeviceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceProtocol.swift; sourceTree = ""; }; CD815B91241DA28B00E2FA97 /* ServerMultiplex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerMultiplex.swift; sourceTree = ""; }; + CD815B93241DC16700E2FA97 /* HubitatServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HubitatServer.swift; sourceTree = ""; }; + CD815B95241DC44D00E2FA97 /* HubitatDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HubitatDevice.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -126,6 +130,7 @@ isa = PBXGroup; children = ( CD815B8F241C7CAC00E2FA97 /* DeviceProtocol.swift */, + CD815B95241DC44D00E2FA97 /* HubitatDevice.swift */, 0C4314FF1C35B1E700164559 /* WemoDevice.swift */, ); path = Models; @@ -227,6 +232,7 @@ children = ( CD815B91241DA28B00E2FA97 /* ServerMultiplex.swift */, CD815B8D241C6F4000E2FA97 /* ServerProtocol.swift */, + CD815B93241DC16700E2FA97 /* HubitatServer.swift */, 0CAA32781C361A2400B353CF /* WemoServer.swift */, ); path = Servers; @@ -318,9 +324,11 @@ 0C41EF4E1C34B81F00AF847F /* KSIRingView.m in Sources */, 0CAA327B1C361CBE00B353CF /* NSErrorAdditions.swift in Sources */, 0CF0F8961C362D8F00FE7752 /* NSURLAdditions.swift in Sources */, + CD815B94241DC16700E2FA97 /* HubitatServer.swift in Sources */, 0CAA32791C361A2400B353CF /* WemoServer.swift in Sources */, 0C41EF561C35ABCD00AF847F /* ConnectionStatusView.swift in Sources */, 0C63A87D1C33C655007E4B52 /* NSValue+XIONAdditions.m in Sources */, + CD815B96241DC44D00E2FA97 /* HubitatDevice.swift in Sources */, 0C20E2BD1C33534F00BA789B /* VisualizationViewController.swift in Sources */, CD815B90241C7CAC00E2FA97 /* DeviceProtocol.swift in Sources */, 0C41EF4A1C34B51500AF847F /* SwitchesViewController.swift in Sources */, diff --git a/XIONControlPanel/Controllers/MainViewController.swift b/XIONControlPanel/Controllers/MainViewController.swift index 4c07bbb..467ab0d 100644 --- a/XIONControlPanel/Controllers/MainViewController.swift +++ b/XIONControlPanel/Controllers/MainViewController.swift @@ -21,6 +21,7 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate, Serv super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) _serverMultiplex.delegate = self + _serverMultiplex.addServer(HubitatServer(URL(string: "https://midna.xionsf.com:6000/apps/api/1/")!)) _serverMultiplex.addServer(WemoServer(URL(string: "http://midna.xionsf.com:5000")!)) } @@ -203,7 +204,7 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate, Serv func serverMultiplex(_ multiplex: ServerMultiplex, didAddDevices devices: [AnyDevice]) { - _switchesController.devices = devices + _switchesController.devices = Array(multiplex.devices) _updateVisualization(false) // Update connection status too, if applicable diff --git a/XIONControlPanel/Controllers/SwitchesViewController.swift b/XIONControlPanel/Controllers/SwitchesViewController.swift index 61d5e0b..b831789 100644 --- a/XIONControlPanel/Controllers/SwitchesViewController.swift +++ b/XIONControlPanel/Controllers/SwitchesViewController.swift @@ -122,6 +122,7 @@ class SwitchesViewController: UIViewController, }) var snapshot = _collectionViewDataSource.snapshot() + snapshot.deleteItems(snapshot.itemIdentifiers(inSection: SwitchesViewController.switchCellsSectionIdentifier)) snapshot.appendItems(self.devices.map { $0.hashValue }, toSection: SwitchesViewController.switchCellsSectionIdentifier) _collectionViewDataSource.apply(snapshot, animatingDifferences: false) } diff --git a/XIONControlPanel/Models/DeviceProtocol.swift b/XIONControlPanel/Models/DeviceProtocol.swift index 515d655..bbdfd0e 100644 --- a/XIONControlPanel/Models/DeviceProtocol.swift +++ b/XIONControlPanel/Models/DeviceProtocol.swift @@ -8,15 +8,15 @@ import Foundation -enum DeviceState +enum DeviceState : String, Codable { - case off - case on + case off = "off" + case on = "on" } -enum DeviceType +enum DeviceType : String, Codable { - case `switch` + case `switch` = "switch" } protocol Device diff --git a/XIONControlPanel/Models/HubitatDevice.swift b/XIONControlPanel/Models/HubitatDevice.swift new file mode 100644 index 0000000..076e4d0 --- /dev/null +++ b/XIONControlPanel/Models/HubitatDevice.swift @@ -0,0 +1,47 @@ +// +// HubitatDevice.swift +// XIONControlPanel +// +// Created by James Magahern on 3/14/20. +// Copyright © 2020 XION. All rights reserved. +// + +import Foundation + +public struct HubitatDevice : Device +{ + var name: String = "" + var serial: String = "" + var type: DeviceType = .switch + var state: DeviceState = .off + + var id: Int = -1 + + enum CodingKeys: String, CodingKey { + case name = "label" + case id = "id" + case attributes = "attributes" + } + + enum AttributesKeys: String, CodingKey { + case power + case dataType + case `switch` + } +} + +extension HubitatDevice : Decodable +{ + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + + let idStr = try values.decode(String.self, forKey: .id) + id = Int(idStr)! + serial = String(format: "hubitat:%d", id) + + name = try values.decode(String.self, forKey: .name) + + let attributes = try values.nestedContainer(keyedBy: Self.AttributesKeys, forKey: .attributes) + state = try attributes.decode(DeviceState.self, forKey: .switch) + } +} diff --git a/XIONControlPanel/Servers/HubitatServer.swift b/XIONControlPanel/Servers/HubitatServer.swift new file mode 100644 index 0000000..33fd94e --- /dev/null +++ b/XIONControlPanel/Servers/HubitatServer.swift @@ -0,0 +1,82 @@ +// +// HubitatServer.swift +// XIONControlPanel +// +// Created by James Magahern on 3/14/20. +// Copyright © 2020 XION. All rights reserved. +// + +import Foundation +import Combine + +class HubitatServer : Server +{ + var connected: Bool = false + + public fileprivate(set) var devices: [HubitatDevice] = [] + + private let baseURL: URL + private let accessToken = "fa861583-71f0-466f-8add-f29b2278af6a" + private var cancellable: AnyCancellable? + + required init(_ url: URL) + { + self.baseURL = url + } + + func connect(_ completion: @escaping (Error?) -> Void) + { + // RESTful, assume we're connected unless we get an error + self.connected = true + completion(nil) + } + + func disconnect(_ completion: (Error?) -> Void) + { + self.connected = false + } + + func fetchDevices(_ fetchCompletion: @escaping (Result<[AnyDevice], Error>) -> Void) + { + let url = baseURL.appendingPathComponent("devices/all").authenticatedURLWithAccessToken(accessToken) + + self.cancellable = URLSession.shared.dataTaskPublisher(for: url) + .mapError { $0 as Error } + .map { $0.data } + .decode(type: [HubitatDevice].self, decoder: JSONDecoder()) + .eraseToAnyPublisher() + .sink(receiveCompletion: { completion in + if case let Subscribers.Completion.failure(error) = completion { + fetchCompletion(.failure(error)) + } + }) { (devices: [HubitatDevice]) in + self.devices = devices + fetchCompletion(.success(devices.map { AnyDevice($0) })) + } + } + + func toggleDevice(_ device: AnyDevice, state: DeviceState, completion: @escaping (Error?) -> Void) + { + if let hubitatDevice = (self.devices.first { $0.serial == device.serial }) { + print("HUBITAT: Toggle: ", hubitatDevice.id) + + // TODO... + } + } + + func responsibleForDevice(_ device: AnyDevice) -> Bool + { + return self.devices.contains { $0.serial == device.serial } + } +} + +extension URL +{ + func authenticatedURLWithAccessToken(_ token: String) -> URL + { + guard var components = URLComponents(url: self, resolvingAgainstBaseURL: false) else { fatalError() } + components.queryItems = [ URLQueryItem(name: "access_token", value: token) ] + + return components.url! + } +} diff --git a/XIONControlPanel/Servers/ServerMultiplex.swift b/XIONControlPanel/Servers/ServerMultiplex.swift index 95d9874..7366866 100644 --- a/XIONControlPanel/Servers/ServerMultiplex.swift +++ b/XIONControlPanel/Servers/ServerMultiplex.swift @@ -102,14 +102,7 @@ extension ServerMultiplex private func handleDevicesChanged(_ devicesChanged: [AnyDevice]) { let newDevicesSet = Set(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 @@ -118,13 +111,19 @@ extension ServerMultiplex return false } + self.devices = self.devices.union(newDevicesSet) + + if additions.count > 0 { + DispatchQueue.main.async { + self.delegate?.serverMultiplex(self, didAddDevices: Array(additions)) + } + } + 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) From a054347f3227e49c7d037c842d8b3be9cac21725 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Thu, 30 Apr 2020 19:00:08 -0700 Subject: [PATCH 5/9] HubitatServer: finishes ability to toggle hubitat switches --- XIONControlPanel/Servers/HubitatServer.swift | 35 ++++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/XIONControlPanel/Servers/HubitatServer.swift b/XIONControlPanel/Servers/HubitatServer.swift index 33fd94e..f593311 100644 --- a/XIONControlPanel/Servers/HubitatServer.swift +++ b/XIONControlPanel/Servers/HubitatServer.swift @@ -9,7 +9,12 @@ import Foundation import Combine -class HubitatServer : Server +public enum HubitatServerError : Error +{ + case apiError +} + +public class HubitatServer : Server { var connected: Bool = false @@ -17,7 +22,8 @@ class HubitatServer : Server private let baseURL: URL private let accessToken = "fa861583-71f0-466f-8add-f29b2278af6a" - private var cancellable: AnyCancellable? + private var fetchCancellable: AnyCancellable? + private var switchCancellable: AnyCancellable? required init(_ url: URL) { @@ -40,7 +46,7 @@ class HubitatServer : Server { let url = baseURL.appendingPathComponent("devices/all").authenticatedURLWithAccessToken(accessToken) - self.cancellable = URLSession.shared.dataTaskPublisher(for: url) + self.fetchCancellable = URLSession.shared.dataTaskPublisher(for: url) .mapError { $0 as Error } .map { $0.data } .decode(type: [HubitatDevice].self, decoder: JSONDecoder()) @@ -55,12 +61,29 @@ class HubitatServer : Server } } - func toggleDevice(_ device: AnyDevice, state: DeviceState, completion: @escaping (Error?) -> Void) + func toggleDevice(_ device: AnyDevice, state: DeviceState, completion toggleCompletion: @escaping (Error?) -> Void) { if let hubitatDevice = (self.devices.first { $0.serial == device.serial }) { - print("HUBITAT: Toggle: ", hubitatDevice.id) + let command = "devices/\(hubitatDevice.id)/\(state)" + let url = baseURL.appendingPathComponent(command).authenticatedURLWithAccessToken(accessToken) - // TODO... + self.switchCancellable = URLSession.shared.dataTaskPublisher(for: url) + .tryMap { data, response -> Data in + guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { + throw HubitatServerError.apiError + } + + return data + } + .decode(type: HubitatDevice.self, decoder: JSONDecoder()) + .sink(receiveCompletion: { (completion: Subscribers.Completion) in + if case let Subscribers.Completion.failure(error) = completion { + toggleCompletion(error) + } + }, receiveValue: { (device: HubitatDevice) in + // don't need to do anything with this, but the server returns it to us. + toggleCompletion(nil) + }) } } From 1bad5ae5b0c86c0cbf60e44c26ae25b89ddd5ab2 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Thu, 30 Apr 2020 23:21:45 -0700 Subject: [PATCH 6/9] Remove unnecessary `eraseToAnyPublisher` --- XIONControlPanel/Servers/HubitatServer.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/XIONControlPanel/Servers/HubitatServer.swift b/XIONControlPanel/Servers/HubitatServer.swift index f593311..e3dbad9 100644 --- a/XIONControlPanel/Servers/HubitatServer.swift +++ b/XIONControlPanel/Servers/HubitatServer.swift @@ -50,7 +50,6 @@ public class HubitatServer : Server .mapError { $0 as Error } .map { $0.data } .decode(type: [HubitatDevice].self, decoder: JSONDecoder()) - .eraseToAnyPublisher() .sink(receiveCompletion: { completion in if case let Subscribers.Completion.failure(error) = completion { fetchCompletion(.failure(error)) From 1cfdf3552fcd3584c77b2ef287316f47c6d5be85 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Thu, 30 Apr 2020 23:36:37 -0700 Subject: [PATCH 7/9] Makes it so we don't have to wait for all servers to be connected before fetching --- XIONControlPanel.xcodeproj/project.pbxproj | 13 ++++-- XIONControlPanel/Servers/HubitatServer.swift | 3 ++ .../Servers/ServerMultiplex.swift | 31 ++----------- XIONControlPanel/Servers/WemoServer.swift | 44 ++++++++++++------- 4 files changed, 42 insertions(+), 49 deletions(-) diff --git a/XIONControlPanel.xcodeproj/project.pbxproj b/XIONControlPanel.xcodeproj/project.pbxproj index 5c2018d..4b55381 100644 --- a/XIONControlPanel.xcodeproj/project.pbxproj +++ b/XIONControlPanel.xcodeproj/project.pbxproj @@ -272,6 +272,7 @@ CreatedOnToolsVersion = 7.2; DevelopmentTeam = F7W5R35V7L; LastSwiftMigration = 1010; + ProvisioningStyle = Manual; }; }; }; @@ -471,14 +472,16 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = "iPhone Developer"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_IDENTITY = "Apple Distribution: Charles Magahern (F7W5R35V7L)"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Distribution: Charles Magahern (F7W5R35V7L)"; + CODE_SIGN_STYLE = Manual; DEVELOPMENT_TEAM = F7W5R35V7L; INFOPLIST_FILE = "$(SRCROOT)/XIONControlPanel/SupportingFiles/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.zanneth.XIONControlPanel; PRODUCT_NAME = XION; PROVISIONING_PROFILE = ""; + PROVISIONING_PROFILE_SPECIFIER = "Octahedron II"; SWIFT_OBJC_BRIDGING_HEADER = "XIONControlPanel/SupportingFiles/XIONControlPanel-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -490,14 +493,16 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = "iPhone Developer"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_IDENTITY = "Apple Distribution: Charles Magahern (F7W5R35V7L)"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Distribution: Charles Magahern (F7W5R35V7L)"; + CODE_SIGN_STYLE = Manual; DEVELOPMENT_TEAM = F7W5R35V7L; INFOPLIST_FILE = "$(SRCROOT)/XIONControlPanel/SupportingFiles/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.zanneth.XIONControlPanel; PRODUCT_NAME = XION; PROVISIONING_PROFILE = ""; + PROVISIONING_PROFILE_SPECIFIER = "Octahedron II"; SWIFT_OBJC_BRIDGING_HEADER = "XIONControlPanel/SupportingFiles/XIONControlPanel-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/XIONControlPanel/Servers/HubitatServer.swift b/XIONControlPanel/Servers/HubitatServer.swift index e3dbad9..5d7eb56 100644 --- a/XIONControlPanel/Servers/HubitatServer.swift +++ b/XIONControlPanel/Servers/HubitatServer.swift @@ -52,7 +52,10 @@ public class HubitatServer : Server .decode(type: [HubitatDevice].self, decoder: JSONDecoder()) .sink(receiveCompletion: { completion in if case let Subscribers.Completion.failure(error) = completion { + self.connected = false fetchCompletion(.failure(error)) + } else { + self.connected = true } }) { (devices: [HubitatDevice]) in self.devices = devices diff --git a/XIONControlPanel/Servers/ServerMultiplex.swift b/XIONControlPanel/Servers/ServerMultiplex.swift index 7366866..53416cc 100644 --- a/XIONControlPanel/Servers/ServerMultiplex.swift +++ b/XIONControlPanel/Servers/ServerMultiplex.swift @@ -54,15 +54,9 @@ class ServerMultiplex 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) - } - } + self.servers.forEach { (server: Server) in + server.fetchDevices { (result: Result<[AnyDevice], Error>) in + self.handleServerFetchResult(result) } } } @@ -70,25 +64,6 @@ class ServerMultiplex 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 { diff --git a/XIONControlPanel/Servers/WemoServer.swift b/XIONControlPanel/Servers/WemoServer.swift index a364cf6..5fc9c16 100644 --- a/XIONControlPanel/Servers/WemoServer.swift +++ b/XIONControlPanel/Servers/WemoServer.swift @@ -26,8 +26,10 @@ class WemoServer : Server { private var devices: [WemoDevice] = [] + public var connected: Bool { get { return self.connectionStatus == .connected } } + fileprivate(set) var baseURL: URL - fileprivate(set) var connected: Bool = false + fileprivate(set) var connectionStatus: ConnectionStatus = .disconnected fileprivate var _urlSession: URLSession fileprivate var _errorStream: StandardErrorOutputStream = StandardErrorOutputStream() @@ -45,15 +47,16 @@ class WemoServer : Server func connect(_ completion: @escaping (Error?) -> Void) { - if (!self.connected) { + if (self.connectionStatus == .disconnected) { 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.connected = true + self.connectionStatus = .connected } completion(strongOp.error) @@ -66,27 +69,34 @@ class WemoServer : Server func disconnect(_ completion: (Error?) -> Void) { - self.connected = false + self.connectionStatus = .disconnected completion(nil) } 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 { return } - if let error = strongOp.error { - self._logError("Error fetching devices", error: error) - completion(.failure(error)) - } else { - self.devices = strongOp.devices - } - + let op = FetchDevicesOperation(baseURL: self.baseURL, session: _urlSession) + weak var weakOp = op + op.completionBlock = { + 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(.success(self.devices.map { AnyDevice($0) })) } + + self.devices = strongOp.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)) From c60685bf2084d1d66dd6999a6aef7270569ac66a Mon Sep 17 00:00:00 2001 From: James Magahern Date: Thu, 30 Apr 2020 23:37:09 -0700 Subject: [PATCH 8/9] Version bump: 2.0 --- XIONControlPanel.xcodeproj/project.pbxproj | 4 ++++ XIONControlPanel/SupportingFiles/Info.plist | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/XIONControlPanel.xcodeproj/project.pbxproj b/XIONControlPanel.xcodeproj/project.pbxproj index 4b55381..e340dcd 100644 --- a/XIONControlPanel.xcodeproj/project.pbxproj +++ b/XIONControlPanel.xcodeproj/project.pbxproj @@ -475,9 +475,11 @@ CODE_SIGN_IDENTITY = "Apple Distribution: Charles Magahern (F7W5R35V7L)"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Distribution: Charles Magahern (F7W5R35V7L)"; CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = F7W5R35V7L; INFOPLIST_FILE = "$(SRCROOT)/XIONControlPanel/SupportingFiles/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MARKETING_VERSION = 2.0; PRODUCT_BUNDLE_IDENTIFIER = com.zanneth.XIONControlPanel; PRODUCT_NAME = XION; PROVISIONING_PROFILE = ""; @@ -496,9 +498,11 @@ CODE_SIGN_IDENTITY = "Apple Distribution: Charles Magahern (F7W5R35V7L)"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Distribution: Charles Magahern (F7W5R35V7L)"; CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = F7W5R35V7L; INFOPLIST_FILE = "$(SRCROOT)/XIONControlPanel/SupportingFiles/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MARKETING_VERSION = 2.0; PRODUCT_BUNDLE_IDENTIFIER = com.zanneth.XIONControlPanel; PRODUCT_NAME = XION; PROVISIONING_PROFILE = ""; diff --git a/XIONControlPanel/SupportingFiles/Info.plist b/XIONControlPanel/SupportingFiles/Info.plist index 6ce6140..9e4a9df 100644 --- a/XIONControlPanel/SupportingFiles/Info.plist +++ b/XIONControlPanel/SupportingFiles/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0 + $(MARKETING_VERSION) CFBundleSignature ???? CFBundleVersion - 1 + $(CURRENT_PROJECT_VERSION) LSRequiresIPhoneOS NSAppTransportSecurity From c6fa9fe96053e7f7ce172f3399b44c11687aa9c3 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Fri, 1 May 2020 00:16:10 -0700 Subject: [PATCH 9/9] Review feedback --- .../Controllers/MainViewController.swift | 4 +++- XIONControlPanel/Models/DeviceProtocol.swift | 15 ++++++++------- XIONControlPanel/Models/HubitatDevice.swift | 6 ++++-- XIONControlPanel/Servers/ServerMultiplex.swift | 6 +++--- XIONControlPanel/Servers/WemoServer.swift | 10 ++++------ 5 files changed, 22 insertions(+), 19 deletions(-) diff --git a/XIONControlPanel/Controllers/MainViewController.swift b/XIONControlPanel/Controllers/MainViewController.swift index 467ab0d..ea39e6d 100644 --- a/XIONControlPanel/Controllers/MainViewController.swift +++ b/XIONControlPanel/Controllers/MainViewController.swift @@ -221,6 +221,8 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate, Serv func serverMultiplex(_ multiplex: ServerMultiplex, didEncounterError error: Error) { _updateConnectivityStatus(.error) - print(error.localizedDescription) + + var stderr = StandardErrorOutputStream() + print(error.localizedDescription, to: &stderr) } } diff --git a/XIONControlPanel/Models/DeviceProtocol.swift b/XIONControlPanel/Models/DeviceProtocol.swift index bbdfd0e..15db6a6 100644 --- a/XIONControlPanel/Models/DeviceProtocol.swift +++ b/XIONControlPanel/Models/DeviceProtocol.swift @@ -10,13 +10,13 @@ import Foundation enum DeviceState : String, Codable { - case off = "off" - case on = "on" + case off + case on } enum DeviceType : String, Codable { - case `switch` = "switch" + case `switch` } protocol Device @@ -50,20 +50,21 @@ class AnyDevice : Device private var device: Device public var name: String - { get { return device.name } } + { return device.name } public var serial: String - { get { return device.serial } } + { return device.serial } public var type: DeviceType - { get { return device.type } } + { return device.type } public var state: DeviceState { get { return device.state } set { device.state = newValue } } - init(_ device: Device) { + init(_ device: Device) + { self.device = device } } diff --git a/XIONControlPanel/Models/HubitatDevice.swift b/XIONControlPanel/Models/HubitatDevice.swift index 076e4d0..72461c6 100644 --- a/XIONControlPanel/Models/HubitatDevice.swift +++ b/XIONControlPanel/Models/HubitatDevice.swift @@ -17,13 +17,15 @@ public struct HubitatDevice : Device var id: Int = -1 - enum CodingKeys: String, CodingKey { + enum CodingKeys: String, CodingKey + { case name = "label" case id = "id" case attributes = "attributes" } - enum AttributesKeys: String, CodingKey { + enum AttributesKeys: String, CodingKey + { case power case dataType case `switch` diff --git a/XIONControlPanel/Servers/ServerMultiplex.swift b/XIONControlPanel/Servers/ServerMultiplex.swift index 53416cc..cdfcfbf 100644 --- a/XIONControlPanel/Servers/ServerMultiplex.swift +++ b/XIONControlPanel/Servers/ServerMultiplex.swift @@ -17,7 +17,7 @@ protocol ServerMultiplexDelegate enum ServerMultiplexError : Error { - case UnknownDevice + case unknownDevice } class ServerMultiplex @@ -26,7 +26,7 @@ class ServerMultiplex public private(set) var devices = Set() private var servers: [Server] = [] - public var numServers: Int { get { return servers.count } } + public var numServers: Int { return servers.count } public func addServer(_ server: Server) { @@ -48,7 +48,7 @@ class ServerMultiplex devices[index].state = state } } else { - completion(ServerMultiplexError.UnknownDevice) + completion(ServerMultiplexError.unknownDevice) } } diff --git a/XIONControlPanel/Servers/WemoServer.swift b/XIONControlPanel/Servers/WemoServer.swift index 5fc9c16..5e688ac 100644 --- a/XIONControlPanel/Servers/WemoServer.swift +++ b/XIONControlPanel/Servers/WemoServer.swift @@ -76,17 +76,15 @@ class WemoServer : Server func fetchDevices(_ completion: @escaping (Result<[AnyDevice], Error>) -> Void) { let op = FetchDevicesOperation(baseURL: self.baseURL, session: _urlSession) - weak var weakOp = op - op.completionBlock = { - guard let strongOp = weakOp else { return } - if let error = strongOp.error { + op.completionBlock = { [unowned op] in + if let error = op.error { self._logError("Error fetching devices", error: error) completion(.failure(error)) } else { - self.devices = strongOp.devices + self.devices = op.devices } - self.devices = strongOp.devices + self.devices = op.devices completion(.success(self.devices.map { AnyDevice($0) })) }