Files
Xaibatsu-Control-Panel/XIONControlPanel/Controllers/MainViewController.swift
James Magahern d1da677932 Fixes issue where connectivity issues can lead to persistent "Offline" status in UI
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
2020-07-04 19:43:50 -07:00

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