// // 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() fileprivate var _audioSectionView: AudioSectionView = AudioSectionView() fileprivate var _audioMixerController: AudioMixerViewController = AudioMixerViewController() fileprivate var _audioMixerDimmingView: UIControl = UIControl(frame: .zero) fileprivate var _isAudioMixerPresented: Bool = false fileprivate var _isAudioMixerAnimating: Bool = false 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) _audioSectionView.mixerSettingsHandler = { [weak self] in self?._presentAudioMixer() } self.view.addSubview(_audioSectionView) _audioMixerController.closeHandler = { [weak self] in self?._dismissAudioMixer() } self.view.addSubview(_headerView) _audioMixerDimmingView.backgroundColor = UIColor.black _audioMixerDimmingView.alpha = 0.0 _audioMixerDimmingView.isHidden = true _audioMixerDimmingView.accessibilityIdentifier = "audio.dimmingView" _audioMixerDimmingView.accessibilityLabel = "Dismiss Mixer" _audioMixerDimmingView.accessibilityTraits = .button _audioMixerDimmingView.addTarget( self, action: #selector(_audioMixerDimmingViewTapped), for: .touchUpInside ) self.view.addSubview(_audioMixerDimmingView) _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 _audioMixerDimmingView.frame = bounds if _visualizationShouldBeVisible() { let sectionSpacing: CGFloat = 8.0 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 ) let rightColumnFrame = CGRect( x: visualizationFrame.maxX, y: bodyBounds.origin.y, width: bodyBounds.width - visualizationFrame.width, height: bodyBounds.height ) let audioContentHeight = min( AudioSectionView.preferredHeight(forWidth: rightColumnFrame.width), bodyBounds.height * 0.22 ) let audioBottomInset = view.safeAreaInsets.bottom let audioHeight = audioContentHeight + audioBottomInset _audioSectionView.bottomContentInset = audioBottomInset _switchesController.view.frame = CGRect( x: rightColumnFrame.minX, y: rightColumnFrame.minY, width: rightColumnFrame.width, height: rightColumnFrame.height - audioHeight - sectionSpacing ) _audioSectionView.frame = CGRect( x: rightColumnFrame.minX, y: _switchesController.view.frame.maxY + sectionSpacing, width: rightColumnFrame.width, height: audioHeight ) } else { let sectionSpacing: CGFloat = 8.0 let audioHeight = min( AudioSectionView.preferredHeight(forWidth: bodyBounds.width), bodyBounds.height * 0.18 ) let lightsHeight = rint(bodyBounds.height * 0.27) let switchesHeight = bodyBounds.height - audioHeight - lightsHeight - (sectionSpacing * 2.0) _visualizationController.view.frame = .zero _audioSectionView.bottomContentInset = 0.0 _switchesController.view.frame = CGRect( x: bodyBounds.minX, y: bodyBounds.minY, width: bodyBounds.width, height: switchesHeight ) _audioSectionView.frame = CGRect( x: bodyBounds.minX + sectionSpacing, y: _switchesController.view.frame.maxY + sectionSpacing, width: bodyBounds.width - (sectionSpacing * 2.0), height: audioHeight ) _lightsController.view.frame = CGRect( x: bodyBounds.minX + sectionSpacing, y: _audioSectionView.frame.maxY + sectionSpacing, width: bodyBounds.width - (sectionSpacing * 2), height: lightsHeight ) } if _audioMixerController.parent === self, !_isAudioMixerAnimating { _audioMixerController.view.frame = _isAudioMixerPresented ? _audioMixerPresentedFrame() : _audioMixerHiddenFrame() } _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 } override var prefersHomeIndicatorAutoHidden: 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 _presentAudioMixer() { guard !_isAudioMixerPresented, !_isAudioMixerAnimating else { return } if _audioMixerController.parent == nil { addChild(_audioMixerController) view.addSubview(_audioMixerController.view) _audioMixerController.didMove(toParent: self) } _isAudioMixerPresented = true _isAudioMixerAnimating = true _audioMixerDimmingView.alpha = 0.0 _audioMixerDimmingView.isHidden = false _audioMixerController.view.isHidden = false _audioMixerController.view.frame = _audioMixerHiddenFrame() _audioMixerController.view.setNeedsLayout() _audioMixerController.view.layoutIfNeeded() view.bringSubviewToFront(_audioMixerDimmingView) view.bringSubviewToFront(_audioMixerController.view) UIView.animate( withDuration: 0.45, delay: 0.0, usingSpringWithDamping: 0.88, initialSpringVelocity: 0.25, options: [.allowUserInteraction, .beginFromCurrentState], animations: { self._audioMixerDimmingView.alpha = 0.55 self._audioMixerController.view.frame = self._audioMixerPresentedFrame() }, completion: { _ in self._isAudioMixerAnimating = false self._audioMixerController.view.frame = self._audioMixerPresentedFrame() } ) } internal func _dismissAudioMixer() { guard _isAudioMixerPresented, !_isAudioMixerAnimating else { return } _isAudioMixerPresented = false _isAudioMixerAnimating = true UIView.animate( withDuration: 0.35, delay: 0.0, options: [.curveEaseIn, .allowUserInteraction, .beginFromCurrentState], animations: { self._audioMixerDimmingView.alpha = 0.0 self._audioMixerController.view.frame = self._audioMixerHiddenFrame() }, completion: { _ in self._isAudioMixerAnimating = false self._audioMixerController.view.isHidden = true self._audioMixerDimmingView.isHidden = true } ) } @objc private func _audioMixerDimmingViewTapped() { _dismissAudioMixer() } private func _audioMixerPresentedFrame() -> CGRect { let switchesFrame = _switchesController.view.frame return CGRect( x: switchesFrame.minX - AudioMixerViewController.controlsWidth, y: switchesFrame.minY, width: switchesFrame.width + AudioMixerViewController.controlsWidth, height: _audioSectionView.frame.maxY - switchesFrame.minY ) } private func _audioMixerHiddenFrame() -> CGRect { return _audioMixerPresentedFrame().offsetBy( dx: UIScreen.main.bounds.width, dy: 0.0 ) } 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 } } }