Server code to fetch and toggle devices

This commit is contained in:
Charles Magahern
2015-12-31 19:51:51 -08:00
parent e85e9173e5
commit e6dd661338
11 changed files with 402 additions and 44 deletions

View File

@@ -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 = "<group>"; };
0C63A8861C33EB3B007E4B52 /* FinalRender.fsh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.glsl; path = FinalRender.fsh; sourceTree = "<group>"; };
0C63A8871C33EB3B007E4B52 /* FinalRender.vsh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.glsl; path = FinalRender.vsh; sourceTree = "<group>"; };
0CAA32781C361A2400B353CF /* WemoServer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WemoServer.swift; sourceTree = "<group>"; };
0CAA327A1C361CBE00B353CF /* NSErrorAdditions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSErrorAdditions.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>"; };
0CF9A48D1C35E9C800276F13 /* SwitchIndicatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwitchIndicatorView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -102,6 +110,7 @@
0C41EF491C34B51500AF847F /* SwitchesViewController.swift */,
0C20E2BA1C33529700BA789B /* MainViewController.swift */,
0C20E2BC1C33534F00BA789B /* VisualizationViewController.swift */,
0CAA32781C361A2400B353CF /* WemoServer.swift */,
);
path = Controllers;
sourceTree = "<group>";
@@ -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 */,
);

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,6 +22,11 @@
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>UIAppFonts</key>
<array>
<string>Orbitron-Medium.ttf</string>

View File

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

View File

@@ -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<String, String> = 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)
}
}

View File

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

View File

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

View File

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