// // 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 _headerView: HeaderView = HeaderView() fileprivate var _refreshTimer: Timer? fileprivate var _isApplicationActive: Bool = false fileprivate var _isViewVisible: Bool = false fileprivate var _switchesController: SwitchesViewController = SwitchesViewController() fileprivate var _lightsController: SwitchesViewController = SwitchesViewController() override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) _serverMultiplex.delegate = self _serverMultiplex.addServer(HomeAssistantServer(URL(string: "https://ha.xaibatsu.com/api")!)) // _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) _lightsController.delegate = self _lightsController.cellHeightScale = 0.7 _lightsController.actionCellLayout = .horizontal _lightsController.labelText = "lights" self.addChild(_lightsController) self.view.addSubview(_lightsController.view) self.view.addSubview(_headerView) _updateConnectivityStatus(.disconnected) _updateSizeClassPresentation() } 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 if _visualizationShouldBeVisible() { let visualizationFrame = CGRect( x: bodyBounds.origin.x, y: bodyBounds.origin.y, width: rint(0.5 * bodyBounds.size.width), height: bodyBounds.size.height / 1.5 ) _visualizationController.view.frame = visualizationFrame let lightsControllerFrame = CGRect( x: bodyBounds.origin.x, y: visualizationFrame.maxY, width: visualizationFrame.width, height: bodyBounds.height - visualizationFrame.height ) _lightsController.view.frame = lightsControllerFrame.insetBy( dx: 18.0, dy: 0.0 ) _switchesController.view.frame = CGRect( x: visualizationFrame.maxX, y: bodyBounds.origin.y, width: bodyBounds.width - visualizationFrame.width, height: bodyBounds.height ) } else { let sectionSpacing: CGFloat = 8 let switchesHeight = rint(bodyBounds.height * 0.68) _visualizationController.view.frame = .zero _switchesController.view.frame = CGRect( x: bodyBounds.minX, y: bodyBounds.minY, width: bodyBounds.width, height: switchesHeight ) _lightsController.view.frame = CGRect( x: bodyBounds.minX + sectionSpacing, y: bodyBounds.minY + switchesHeight + sectionSpacing, width: bodyBounds.width - (sectionSpacing * 2), height: bodyBounds.height - switchesHeight - sectionSpacing ) } _updateSizeClassPresentation() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) _isViewVisible = true _updateActiveState() } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) _isViewVisible = false _updateActiveState() } 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 ) { [weak self] error in guard let error else { return } DispatchQueue.main.async { guard let self else { return } self._updateConnectivityStatus(.error) self._serverMultiplex.refreshDevices() var stderr = StandardErrorOutputStream() print(error.localizedDescription, to: &stderr) } } } } // 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 applicationDidBecomeActive() { _isApplicationActive = true _updateActiveState() } internal func applicationWillResignActive() { _isApplicationActive = false _updateActiveState() } private func _updateActiveState() { let shouldBeActive = _isApplicationActive && _isViewVisible if shouldBeActive, _refreshTimer == nil { UIApplication.shared.isIdleTimerDisabled = true _headerView.xionLogoView.beginAnimating() _updateConnectivityStatus(.connecting) _serverMultiplex.refreshDevices() let timer = Timer(timeInterval: 10, repeats: true) { [weak self] _ in self?._serverMultiplex.refreshDevices() } RunLoop.main.add(timer, forMode: .common) _refreshTimer = timer } else if !shouldBeActive, _refreshTimer != nil { _refreshTimer?.invalidate() _refreshTimer = nil _serverMultiplex.disconnect() _updateConnectivityStatus(.disconnected) _headerView.xionLogoView.stopAnimating() UIApplication.shared.isIdleTimerDisabled = false } } // MARK: Server Multiplex Delegate func serverMultiplex(_ multiplex: ServerMultiplex, didAddDevices devices: [AnyDevice]) { _switchesController.devices = multiplex.switchDevices _lightsController.devices = multiplex.lightDevices _updateVisualization(false) } func serverMultiplex(_ multiplex: ServerMultiplex, devicesStateChanged devices: [AnyDevice]) { _switchesController.devicesStateChanged(devices.switches) _lightsController.devicesStateChanged(devices.lights) _updateVisualization(true) } func serverMultiplex(_ multiplex: ServerMultiplex, didReceiveAcknowledgementFromServer server: Server) { // Update connection status too, if applicable let groupConnectionStatus = multiplex.groupConnectionStatus() _updateConnectivityStatus(groupConnectionStatus) } func serverMultiplexConnectionStatusDidChange(_ multiplex: ServerMultiplex) { _updateConnectivityStatus(multiplex.groupConnectionStatus()) } func serverMultiplex(_ multiplex: ServerMultiplex, didEncounterError error: Error) { _updateConnectivityStatus(.error) var stderr = StandardErrorOutputStream() print(error.localizedDescription, to: &stderr) } } extension Array where Element == AnyDevice { var lights: [AnyDevice] { filter { $0.type == .light } } var switches: [AnyDevice] { filter { $0.type == .switch } } } extension ServerMultiplex { var lightDevices: [AnyDevice] { devices.filter { $0.type == .light } } var switchDevices: [AnyDevice] { devices.filter { $0.type == .switch } } }