diff --git a/XIONControlPanel.xcodeproj/project.pbxproj b/XIONControlPanel.xcodeproj/project.pbxproj index 14a174f..d8f51ab 100644 --- a/XIONControlPanel.xcodeproj/project.pbxproj +++ b/XIONControlPanel.xcodeproj/project.pbxproj @@ -30,6 +30,10 @@ 0C63A8851C33DC29007E4B52 /* VerticalBlur.vsh in Resources */ = {isa = PBXBuildFile; fileRef = 0C63A8831C33DC29007E4B52 /* VerticalBlur.vsh */; }; 0C63A8881C33EB3B007E4B52 /* FinalRender.fsh in Resources */ = {isa = PBXBuildFile; fileRef = 0C63A8861C33EB3B007E4B52 /* FinalRender.fsh */; }; 0C63A8891C33EB3B007E4B52 /* FinalRender.vsh in Resources */ = {isa = PBXBuildFile; fileRef = 0C63A8871C33EB3B007E4B52 /* FinalRender.vsh */; }; + 0CAA32791C361A2400B353CF /* WemoServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAA32781C361A2400B353CF /* WemoServer.swift */; }; + 0CAA327B1C361CBE00B353CF /* NSErrorAdditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAA327A1C361CBE00B353CF /* NSErrorAdditions.swift */; }; + 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 */; }; /* End PBXBuildFile section */ @@ -62,6 +66,10 @@ 0C63A8831C33DC29007E4B52 /* VerticalBlur.vsh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.glsl; path = VerticalBlur.vsh; sourceTree = ""; }; 0C63A8861C33EB3B007E4B52 /* FinalRender.fsh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.glsl; path = FinalRender.fsh; sourceTree = ""; }; 0C63A8871C33EB3B007E4B52 /* FinalRender.vsh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.glsl; path = FinalRender.vsh; sourceTree = ""; }; + 0CAA32781C361A2400B353CF /* WemoServer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WemoServer.swift; sourceTree = ""; }; + 0CAA327A1C361CBE00B353CF /* NSErrorAdditions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSErrorAdditions.swift; sourceTree = ""; }; + 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 = ""; }; /* End PBXFileReference section */ @@ -102,6 +110,7 @@ 0C41EF491C34B51500AF847F /* SwitchesViewController.swift */, 0C20E2BA1C33529700BA789B /* MainViewController.swift */, 0C20E2BC1C33534F00BA789B /* VisualizationViewController.swift */, + 0CAA32781C361A2400B353CF /* WemoServer.swift */, ); path = Controllers; sourceTree = ""; @@ -194,6 +203,9 @@ 0C63A8791C33C622007E4B52 /* Utilities */ = { isa = PBXGroup; children = ( + 0CAA327C1C361ECA00B353CF /* StandardError.swift */, + 0CAA327A1C361CBE00B353CF /* NSErrorAdditions.swift */, + 0CF0F8951C362D8F00FE7752 /* NSURLAdditions.swift */, 0C63A87B1C33C655007E4B52 /* NSValue+XIONAdditions.h */, 0C63A87C1C33C655007E4B52 /* NSValue+XIONAdditions.m */, ); @@ -282,6 +294,9 @@ buildActionMask = 2147483647; files = ( 0C41EF4E1C34B81F00AF847F /* KSIRingView.m in Sources */, + 0CAA327B1C361CBE00B353CF /* NSErrorAdditions.swift in Sources */, + 0CF0F8961C362D8F00FE7752 /* NSURLAdditions.swift in Sources */, + 0CAA32791C361A2400B353CF /* WemoServer.swift in Sources */, 0C41EF561C35ABCD00AF847F /* ConnectionStatusView.swift in Sources */, 0C63A87D1C33C655007E4B52 /* NSValue+XIONAdditions.m in Sources */, 0C20E2BD1C33534F00BA789B /* VisualizationViewController.swift in Sources */, @@ -291,6 +306,7 @@ 0C41EF501C34B97700AF847F /* HeaderView.swift in Sources */, 0C41EF541C34BC9800AF847F /* XIONLogoView.swift in Sources */, 0C4315061C35E67600164559 /* WemoCellView.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 1bad214..5f0468b 100644 --- a/XIONControlPanel/Controllers/MainViewController.swift +++ b/XIONControlPanel/Controllers/MainViewController.swift @@ -9,10 +9,24 @@ import UIKit class MainViewController: UIViewController, SwitchesViewControllerDelegate { + private var _server: WemoServer private var _visualizationController: VisualizationViewController = VisualizationViewController() private var _switchesController: SwitchesViewController = SwitchesViewController() private var _headerView: HeaderView = HeaderView() + override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) + { + let url = NSURL(string: "http://10.0.1.4:8000") + _server = WemoServer(url!) + + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + } + + required init?(coder aDecoder: NSCoder) + { + fatalError("unsupported") + } + // MARK: Overrides override func viewDidLoad() @@ -30,12 +44,7 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate { self.view.addSubview(_headerView) - // DEBUG - for _ in 0 ..< 10 { - let device = WemoDevice() - device.name = "beatmania IIDX" - self.devices.append(device) - } + _updateConnectivityStatus(.Disconnected) } override func viewDidLayoutSubviews() @@ -78,6 +87,15 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate { override func viewDidAppear(animated: Bool) { _headerView.xionLogoView.beginAnimating() + + _updateConnectivityStatus(.Connecting) + _server.connect { (error: NSError?) -> Void in + if (error == nil) { + self._reloadDevices() + } else { + self._updateConnectivityStatus(.Error) + } + } } override func viewDidDisappear(animated: Bool) @@ -106,6 +124,10 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate { func switchesViewControllerDidToggleDevices(controller: SwitchesViewController, devices: [WemoDevice]) { _updateVisualization(true) + + for device in devices { + _server.toggleDevice(device, state: device.state, completion: { (error: NSError?) -> Void in }) + } } // MARK: Internal @@ -124,4 +146,26 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate { _visualizationController.setPercentActivated(percentageActivated, animated: animated) } } + + internal func _updateConnectivityStatus(status: ConnectionStatus) + { + dispatch_async(dispatch_get_main_queue()) { () -> Void in + self._headerView.connectionStatusView.connectivityStatus = status + } + } + + internal func _reloadDevices() + { + _server.fetchDevices({ (devices: [WemoDevice], error: NSError?) -> Void in + dispatch_async(dispatch_get_main_queue()) { () -> Void in + if (error == nil) { + self.devices = devices + self._updateConnectivityStatus(.Connected) + } else { + self.devices = [] + self._updateConnectivityStatus(.Error) + } + } + }) + } } diff --git a/XIONControlPanel/Controllers/SwitchesViewController.swift b/XIONControlPanel/Controllers/SwitchesViewController.swift index ec116a4..5a3cbd9 100644 --- a/XIONControlPanel/Controllers/SwitchesViewController.swift +++ b/XIONControlPanel/Controllers/SwitchesViewController.swift @@ -69,6 +69,8 @@ class SwitchesViewController: UIViewController, _collectionView.registerClass(WemoDeviceCellView.self, forCellWithReuseIdentifier: deviceCellReuseID) _collectionView.registerClass(WemoActionCellView.self, forCellWithReuseIdentifier: actionCellReuseID) self.view.addSubview(_collectionView) + + self.devices = [] } override func viewDidLayoutSubviews() @@ -102,6 +104,7 @@ class SwitchesViewController: UIViewController, let reuseID = SwitchesViewController.collectionViewActionCellReuseIdentifier let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseID, forIndexPath: indexPath) as! WemoActionCellView cell.textLabel.text = ActionCell(rawValue: indexPath.item)?.name().uppercaseString + cell.enabled = (self.devices.count > 0) return cell } else { @@ -136,8 +139,8 @@ class SwitchesViewController: UIViewController, { if (indexPath.item < ActionCell.count) { let tappedActionCell = ActionCell(rawValue: indexPath.item) - var currentDelay: NSTimeInterval = 0.0 + for var i = ActionCell.count; i < collectionView.numberOfItemsInSection(indexPath.section); ++i { let cell = collectionView.cellForItemAtIndexPath(NSIndexPath(forItem: i, inSection: indexPath.section)) as! WemoDeviceCellView @@ -164,4 +167,22 @@ class SwitchesViewController: UIViewController, self.delegate?.switchesViewControllerDidToggleDevices(self, devices: [device]) } } + + func collectionView(collectionView: UICollectionView, shouldHighlightItemAtIndexPath indexPath: NSIndexPath) -> Bool + { + if (indexPath.item < ActionCell.count) { + return (self.devices.count > 0) + } else { + return true + } + } + + func collectionView(collectionView: UICollectionView, shouldSelectItemAtIndexPath indexPath: NSIndexPath) -> Bool + { + if (indexPath.item < ActionCell.count) { + return (self.devices.count > 0) + } else { + return true + } + } } diff --git a/XIONControlPanel/Controllers/WemoServer.swift b/XIONControlPanel/Controllers/WemoServer.swift new file mode 100644 index 0000000..6b25c8a --- /dev/null +++ b/XIONControlPanel/Controllers/WemoServer.swift @@ -0,0 +1,134 @@ +// +// WemoServer.swift +// XIONControlPanel +// +// Created by Charles Magahern on 12/31/15. +// Copyright © 2015 XION. All rights reserved. +// + +import Foundation + +enum ConnectionStatus { + case Disconnected + case Connecting + case Connected + case Error +} + +class WemoServer { + private(set) var baseURL: NSURL + private(set) var connected: Bool = false + + private var _urlSession: NSURLSession + private var _errorStream: StandardErrorOutputStream = StandardErrorOutputStream() + + init(_ url: NSURL) + { + self.baseURL = url + + let config = NSURLSessionConfiguration.defaultSessionConfiguration() + _urlSession = NSURLSession(configuration: config) + } + + func connect(completion: (NSError?) -> Void) + { + if (!self.connected) { + let url = self.baseURL.URLByAppendingPathComponent("api/environment") + let request = NSMutableURLRequest(URL: url) + request.HTTPMethod = "POST" + + let task = _urlSession.dataTaskWithRequest(request) { (data: NSData?, response: NSURLResponse?, error: NSError?) -> Void in + var clientError: NSError? = nil + + if (error != nil) { + clientError = NSError.xionError(.ConnectionError, underlying: error!) + self._logError("Error connecting to server", error: clientError!) + } else { + self.connected = true + } + + completion(clientError) + } + task.resume() + } else { + completion(nil) + } + } + + func disconnect(completion: (NSError?) -> Void) + { + self.connected = false + completion(nil) + } + + func fetchDevices(completion: ([WemoDevice], NSError?) -> Void) + { + if (self.connected) { + let url = self.baseURL.URLByAppendingPathComponent("api/environment") + let task = _urlSession.dataTaskWithURL(url) { (data: NSData?, response: NSURLResponse?, error: NSError?) -> Void in + var devices: [WemoDevice] = [] + var clientError: NSError? = nil + + if (data != nil) { + devices = self._parseDevices(data!) + } else { + clientError = NSError.xionError(.ConnectionError, underlying: error) + self._logError("Error fetching devices", error: clientError!) + } + + completion(devices, clientError) + } + task.resume() + } else { + let err = NSError.xionError(.ConnectionError) + completion([], err) + } + } + + func toggleDevice(device: WemoDevice, state: WemoDevice.State, completion: (NSError?) -> Void) + { + if (self.connected) { + let stateArg = (state == .On ? "on" : "off") + let url = self.baseURL.URLByAppendingPathComponent("api/device/\(device.name)").URLByAppendingRequestParameters(["state" : stateArg]) + let request = NSMutableURLRequest(URL: url!) + request.HTTPMethod = "POST" + + let task = _urlSession.dataTaskWithRequest(request) { (data: NSData?, response: NSURLResponse?, error: NSError?) -> Void in + var clientError: NSError? = nil + + if (error != nil) { + clientError = NSError.xionError(.ConnectionError, underlying: error) + } + + completion(clientError) + } + task.resume() + } else { + let err = NSError.xionError(.ConnectionError) + completion(err) + } + } + + // MARK: Internal + + internal func _logError(description: String, error: NSError) + { + print("ERROR: \(description) \(error)", toStream: &_errorStream) + } + + internal func _parseDevices(data: NSData) -> [WemoDevice] + { + var devices: [WemoDevice] = [] + + if let responseDict = (try? NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions()) as? NSDictionary) { + for responseObj in (responseDict?.allValues)! { + if let responseDict = responseObj as? NSDictionary { + let device = WemoDevice(responseDict) + devices.append(device) + } + } + } + + return devices + } +} diff --git a/XIONControlPanel/Models/WemoDevice.swift b/XIONControlPanel/Models/WemoDevice.swift index 77194e8..9912e3e 100644 --- a/XIONControlPanel/Models/WemoDevice.swift +++ b/XIONControlPanel/Models/WemoDevice.swift @@ -28,35 +28,33 @@ class WemoDevice { init() {} - init(_ responseData: NSData) + init(_ dict: NSDictionary) { - if let dict = (try? NSJSONSerialization.JSONObjectWithData(responseData, options: NSJSONReadingOptions())) as? NSDictionary { - if let name = dict["name"] as? NSString { - self.name = String(name) - } - - if let host = dict["host"] as? NSString { - self.host = String(host) - } - - if let model = dict["model"] as? NSString { - self.model = String(model) - } - - if let state = dict["state"] as? NSNumber { - switch (state.integerValue) { - case 0: - self.state = .Off - case 1: - self.state = .On - default: - self.state = .Off - } - } - - if let serial = dict["serialnumber"] as? NSString { - self.serial = String(serial) + if let name = dict["name"] as? NSString { + self.name = String(name) + } + + if let host = dict["host"] as? NSString { + self.host = String(host) + } + + if let model = dict["model"] as? NSString { + self.model = String(model) + } + + if let state = dict["state"] as? NSNumber { + switch (state.integerValue) { + case 0: + self.state = .Off + case 1: + self.state = .On + default: + self.state = .Off } } + + if let serial = dict["serialnumber"] as? NSString { + self.serial = String(serial) + } } } diff --git a/XIONControlPanel/SupportingFiles/Info.plist b/XIONControlPanel/SupportingFiles/Info.plist index 0e68bad..44c3e22 100644 --- a/XIONControlPanel/SupportingFiles/Info.plist +++ b/XIONControlPanel/SupportingFiles/Info.plist @@ -22,6 +22,11 @@ 1 LSRequiresIPhoneOS + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + UIAppFonts Orbitron-Medium.ttf diff --git a/XIONControlPanel/Utilities/NSErrorAdditions.swift b/XIONControlPanel/Utilities/NSErrorAdditions.swift new file mode 100644 index 0000000..dc06879 --- /dev/null +++ b/XIONControlPanel/Utilities/NSErrorAdditions.swift @@ -0,0 +1,45 @@ +// +// NSErrorAdditions.swift +// XIONControlPanel +// +// Created by Charles Magahern on 12/31/15. +// Copyright © 2015 XION. All rights reserved. +// + +import Foundation + +enum XIONErrorCode: Int { + case Unknown + case ConnectionError +} + +extension NSError { + private static let XIONErrorDomain = "com.xionsf.controlpanel" + + class func xionError(code: XIONErrorCode) -> NSError + { + return self.xionError(code, userInfo: nil) + } + + class func xionError(code: XIONErrorCode, underlying: NSError?) -> NSError + { + var userInfo: [NSObject : AnyObject]? = nil + if (underlying != nil) { + userInfo = [ + NSUnderlyingErrorKey : underlying! + ] + } + + return self.xionError(code, userInfo: userInfo) + } + + class func xionError(code: XIONErrorCode, userInfo: [NSObject : AnyObject]?) -> NSError + { + return NSError(domain: XIONErrorDomain, code: code.rawValue, userInfo: userInfo) + } + + var xionErrorCode: XIONErrorCode + { + return XIONErrorCode(rawValue: self.code)! + } +} diff --git a/XIONControlPanel/Utilities/NSURLAdditions.swift b/XIONControlPanel/Utilities/NSURLAdditions.swift new file mode 100644 index 0000000..09cffe7 --- /dev/null +++ b/XIONControlPanel/Utilities/NSURLAdditions.swift @@ -0,0 +1,54 @@ +// +// NSURLAdditions.swift +// ArchillectTV +// +// Created by Charles Magahern on 10/25/15. +// + +import Foundation + +extension NSURL { + var requestParameters: [String : String]? { + get + { + let urlComponents = self.absoluteString.componentsSeparatedByString("/") + let paramsString = urlComponents[urlComponents.count - 1] + let parameterComponents = paramsString.componentsSeparatedByCharactersInSet(NSCharacterSet(charactersInString: "&?")) + var dict: Dictionary = Dictionary() + + for paramPair in parameterComponents { + let kvPair = paramPair.componentsSeparatedByString("=") + if (kvPair.count == 2 && kvPair[0].characters.count > 0) { + let key = kvPair[0].stringByRemovingPercentEncoding! + let value = kvPair[1].stringByRemovingPercentEncoding! + dict[key] = value + } + } + + return dict + } + } + + func URLByAppendingRequestParameters(params: [String : String]) -> NSURL? + { + var paramsString = "" + + for (key, value) in params { + var delim = "" + if (paramsString.characters.count == 0) { + delim = "?" + } else { + delim = "&" + } + + let keyParam = key.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet()) + let valueParam = value.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet()) + paramsString += "\(delim)\(keyParam!)=\(valueParam!)" + } + + var absoluteString = self.absoluteString + absoluteString += paramsString + + return NSURL(string: absoluteString) + } +} diff --git a/XIONControlPanel/Utilities/StandardError.swift b/XIONControlPanel/Utilities/StandardError.swift new file mode 100644 index 0000000..261741b --- /dev/null +++ b/XIONControlPanel/Utilities/StandardError.swift @@ -0,0 +1,16 @@ +// +// StandardError.swift +// XIONControlPanel +// +// Created by Charles Magahern on 12/31/15. +// Copyright © 2015 XION. All rights reserved. +// + +import Foundation + +class StandardErrorOutputStream: OutputStreamType { + func write(string: String) { + let stderr = NSFileHandle.fileHandleWithStandardError() + stderr.writeData(string.dataUsingEncoding(NSUTF8StringEncoding)!) + } +} diff --git a/XIONControlPanel/Views/ConnectionStatusView.swift b/XIONControlPanel/Views/ConnectionStatusView.swift index 2d17845..16998d9 100644 --- a/XIONControlPanel/Views/ConnectionStatusView.swift +++ b/XIONControlPanel/Views/ConnectionStatusView.swift @@ -9,9 +9,9 @@ import UIKit class ConnectionStatusView: UIView { - private var _connectivityStatus:Bool = false - private var _japaneseLabel: UILabel = UILabel() - private var _englishLabel: UILabel = UILabel() + private var _connectivityStatus: ConnectionStatus = .Disconnected + private var _japaneseLabel: UILabel = UILabel() + private var _englishLabel: UILabel = UILabel() static private let labelsVMargin: CGFloat = 5.0 @@ -27,7 +27,7 @@ class ConnectionStatusView: UIView { _englishLabel.textColor = UIColor.whiteColor() self.addSubview(_englishLabel) - self.connectivityStatus = false + self.connectivityStatus = .Disconnected } required init?(coder aDecoder: NSCoder) @@ -74,7 +74,7 @@ class ConnectionStatusView: UIView { // MARK: API - var connectivityStatus: Bool + var connectivityStatus: ConnectionStatus { get { @@ -85,17 +85,30 @@ class ConnectionStatusView: UIView { { _connectivityStatus = newStatus - if (_connectivityStatus == true) { + switch (_connectivityStatus) { + case .Disconnected: + _japaneseLabel.text = "非直結" + _japaneseLabel.textColor = UIColor.redColor() + + _englishLabel.text = "offline" + _englishLabel.textColor = UIColor.redColor() + case .Connecting: + _japaneseLabel.text = "接続中" + _japaneseLabel.textColor = UIColor.yellowColor() + + _englishLabel.text = "connecting..." + _englishLabel.textColor = UIColor.yellowColor() + case .Connected: _japaneseLabel.text = "直結" _japaneseLabel.textColor = UIColor.greenColor() _englishLabel.text = "online" _englishLabel.textColor = UIColor.greenColor() - } else { - _japaneseLabel.text = "非直結" + case .Error: + _japaneseLabel.text = "過失" _japaneseLabel.textColor = UIColor.redColor() - _englishLabel.text = "offline" + _englishLabel.text = "error" _englishLabel.textColor = UIColor.redColor() } diff --git a/XIONControlPanel/Views/WemoCellView.swift b/XIONControlPanel/Views/WemoCellView.swift index 1ba2c58..44005b8 100644 --- a/XIONControlPanel/Views/WemoCellView.swift +++ b/XIONControlPanel/Views/WemoCellView.swift @@ -175,7 +175,7 @@ public class WemoDeviceCellView: WemoCellView { } public class WemoActionCellView: WemoCellView { - public var textLabel: UILabel = UILabel() + var textLabel: UILabel = UILabel() override init(frame: CGRect) { @@ -197,4 +197,16 @@ public class WemoActionCellView: WemoCellView { super.layoutSubviews() self.textLabel.frame = self.bounds } + + var enabled: Bool = true + { + didSet + { + if (enabled) { + self.textLabel.textColor = UIColor.whiteColor() + } else { + self.textLabel.textColor = UIColor.darkGrayColor() + } + } + } }