This fixes the problem where either internet connectivity issues on the device or intermittant server issues can cause a persistent "OFFLINE" indicator even though that is not the case. This requires the server protocol to be more granular about what their connection status is, and adds a new signal from the multiplexer to the MainViewController allowing it to notify the delegate that an acknowledgement was heard from one of the servers, allowing the main view controller to update its UI according to whatever status best represents the group of servers as a whole. Testing - This can be tested by using the network link conditioner to simulate connection issues *after* the control panel has connected to all servers in the multiplex. - Tested losing internet connection and regaining internet connection - Tested starting off with no connection and regaining connection later
231 lines
7.3 KiB
Swift
231 lines
7.3 KiB
Swift
//
|
|
// MainViewController.swift
|
|
// XIONControlPanel
|
|
//
|
|
// Created by Charles Magahern on 12/29/15.
|
|
// Copyright © 2015 XION. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
|
|
class MainViewController: UIViewController, SwitchesViewControllerDelegate, ServerMultiplexDelegate
|
|
{
|
|
fileprivate var _serverMultiplex: ServerMultiplex = ServerMultiplex()
|
|
fileprivate var _visualizationController: VisualizationViewController = VisualizationViewController()
|
|
fileprivate var _switchesController: SwitchesViewController = SwitchesViewController()
|
|
fileprivate var _headerView: HeaderView = HeaderView()
|
|
fileprivate var _updateDevices: Bool = false
|
|
|
|
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?)
|
|
{
|
|
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")!))
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder)
|
|
{
|
|
fatalError("unsupported")
|
|
}
|
|
|
|
// MARK: Overrides
|
|
|
|
override func viewDidLoad()
|
|
{
|
|
super.viewDidLoad()
|
|
|
|
self.view.backgroundColor = UIColor.black
|
|
|
|
self.addChild(_visualizationController)
|
|
self.view.addSubview(_visualizationController.view)
|
|
|
|
_switchesController.delegate = self
|
|
self.addChild(_switchesController)
|
|
self.view.addSubview(_switchesController.view)
|
|
|
|
self.view.addSubview(_headerView)
|
|
|
|
_updateConnectivityStatus(.disconnected)
|
|
}
|
|
|
|
override func viewDidLayoutSubviews()
|
|
{
|
|
super.viewDidLayoutSubviews()
|
|
|
|
let bounds = self.view.bounds
|
|
let headerBounds = CGRect(
|
|
x: 0.0,
|
|
y: 0.0,
|
|
width: bounds.size.width,
|
|
height: rint(bounds.size.height / 8.0)
|
|
)
|
|
let bodyBounds = CGRect(
|
|
x: 0.0,
|
|
y: headerBounds.maxY,
|
|
width: bounds.size.width,
|
|
height: bounds.size.height - headerBounds.size.height
|
|
)
|
|
|
|
_headerView.frame = headerBounds
|
|
|
|
let visualizationFrame = CGRect(
|
|
x: bodyBounds.origin.x,
|
|
y: bodyBounds.origin.y,
|
|
width: rint(0.5 * bodyBounds.size.width),
|
|
height: bodyBounds.size.height
|
|
)
|
|
_visualizationController.view.frame = visualizationFrame
|
|
|
|
var switchesOriginX: CGFloat = 0.0
|
|
var switchesWidth: CGFloat = 0.0
|
|
if (_visualizationShouldBeVisible()) {
|
|
switchesOriginX = visualizationFrame.maxX
|
|
switchesWidth = bodyBounds.size.width - visualizationFrame.size.width
|
|
} else {
|
|
switchesOriginX = 0.0
|
|
switchesWidth = bodyBounds.size.width
|
|
}
|
|
|
|
let switchesControllerFrame = CGRect(
|
|
x: switchesOriginX,
|
|
y: bodyBounds.origin.y,
|
|
width: switchesWidth,
|
|
height: bodyBounds.size.height
|
|
)
|
|
_switchesController.view.frame = switchesControllerFrame
|
|
}
|
|
|
|
override func viewDidAppear(_ animated: Bool)
|
|
{
|
|
super.viewDidAppear(animated)
|
|
|
|
UIApplication.shared.isIdleTimerDisabled = true
|
|
|
|
_headerView.xionLogoView.beginAnimating()
|
|
|
|
if _serverMultiplex.devices.count == 0 {
|
|
// Do initial refresh
|
|
_updateConnectivityStatus(.connecting)
|
|
_serverMultiplex.refreshDevices()
|
|
_startUpdatingDevices()
|
|
}
|
|
}
|
|
|
|
override func viewDidDisappear(_ animated: Bool)
|
|
{
|
|
super.viewDidDisappear(animated)
|
|
UIApplication.shared.isIdleTimerDisabled = false
|
|
}
|
|
|
|
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator)
|
|
{
|
|
super.viewWillTransition(to: size, with: coordinator)
|
|
_updateSizeClassPresentation()
|
|
}
|
|
|
|
override var prefersStatusBarHidden : Bool
|
|
{
|
|
return true
|
|
}
|
|
|
|
// MARK: SwitchesViewControllerDelegate
|
|
|
|
func switchesViewControllerDidToggleDevices(_ controller: SwitchesViewController, devices: [AnyDevice])
|
|
{
|
|
_updateVisualization(true)
|
|
|
|
for device in devices {
|
|
_serverMultiplex.toggleDeviceState(device, state: device.state, completion: { (error: Error?) -> Void in })
|
|
}
|
|
}
|
|
|
|
// MARK: Internal
|
|
|
|
internal func _visualizationShouldBeVisible() -> Bool
|
|
{
|
|
switch self.traitCollection.horizontalSizeClass {
|
|
case .regular:
|
|
return true
|
|
|
|
case .compact:
|
|
return false
|
|
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
internal func _updateSizeClassPresentation()
|
|
{
|
|
_visualizationController.view.isHidden = !_visualizationShouldBeVisible()
|
|
}
|
|
|
|
internal func _updateVisualization(_ animated: Bool)
|
|
{
|
|
let activatedDevicesCount = _serverMultiplex.devices.filter { $0.state == .on }.count
|
|
let totalDeviceCount = _serverMultiplex.devices.count
|
|
|
|
let percentageActivated = (totalDeviceCount > 0 ? Float(activatedDevicesCount) / Float(totalDeviceCount) : 0.0)
|
|
if (percentageActivated != _visualizationController.percentActivated) {
|
|
_visualizationController.setPercentActivated(percentageActivated, animated: animated)
|
|
}
|
|
}
|
|
|
|
internal func _updateConnectivityStatus(_ status: ConnectionStatus)
|
|
{
|
|
DispatchQueue.main.async { () -> Void in
|
|
self._headerView.connectionStatusView.connectivityStatus = status
|
|
self._headerView.setNeedsLayout()
|
|
self._visualizationController.connectionStatus = status
|
|
}
|
|
}
|
|
|
|
internal func _startUpdatingDevices()
|
|
{
|
|
_updateDevices = true
|
|
|
|
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._serverMultiplex.refreshDevices()
|
|
self._startUpdatingDevices()
|
|
}
|
|
}
|
|
}
|
|
|
|
internal func _stopUpdatingDevices()
|
|
{
|
|
_updateDevices = false
|
|
}
|
|
|
|
// MARK: Server Multiplex Delegate
|
|
|
|
func serverMultiplex(_ multiplex: ServerMultiplex, didAddDevices devices: [AnyDevice])
|
|
{
|
|
_switchesController.devices = Array(multiplex.devices)
|
|
_updateVisualization(false)
|
|
}
|
|
|
|
func serverMultiplex(_ multiplex: ServerMultiplex, devicesStateChanged devices: [AnyDevice])
|
|
{
|
|
_switchesController.devicesStateChanged(devices)
|
|
}
|
|
|
|
func serverMultiplex(_ multiplex: ServerMultiplex, didReceiveAcknowledgementFromServer server: Server)
|
|
{
|
|
// Update connection status too, if applicable
|
|
let groupConnectionStatus = multiplex.groupConnectionStatus()
|
|
_updateConnectivityStatus(groupConnectionStatus)
|
|
}
|
|
|
|
func serverMultiplex(_ multiplex: ServerMultiplex, didEncounterError error: Error)
|
|
{
|
|
_updateConnectivityStatus(.error)
|
|
|
|
var stderr = StandardErrorOutputStream()
|
|
print(error.localizedDescription, to: &stderr)
|
|
}
|
|
}
|