403 lines
14 KiB
Swift
403 lines
14 KiB
Swift
import CoreLocation
|
|
import Eureka
|
|
import Foundation
|
|
import PromiseKit
|
|
import Shared
|
|
|
|
final class ConnectionURLViewController: HAFormViewController, TypedRowControllerType {
|
|
typealias RowValue = ConnectionURLViewController
|
|
var row: RowOf<RowValue>!
|
|
var onDismissCallback: ((UIViewController) -> Void)?
|
|
let urlType: ConnectionInfo.URLType
|
|
let server: Server
|
|
|
|
init(server: Server, urlType: ConnectionInfo.URLType, row: RowOf<RowValue>) {
|
|
self.server = server
|
|
self.urlType = urlType
|
|
self.row = row
|
|
|
|
super.init()
|
|
|
|
self.title = urlType.description
|
|
self.isModalInPresentation = true
|
|
}
|
|
|
|
enum SaveError: LocalizedError {
|
|
case lastURL
|
|
case validation([ValidationError])
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .lastURL: return L10n.Settings.ConnectionSection.Errors.cannotRemoveLastUrl
|
|
case let .validation(errors): return errors.map(\.msg).joined(separator: "\n")
|
|
}
|
|
}
|
|
|
|
var isFinal: Bool {
|
|
switch self {
|
|
case .lastURL: return true
|
|
case .validation: return true
|
|
}
|
|
}
|
|
}
|
|
|
|
private func check(url: URL?, useCloud: Bool?, validationErrors: [ValidationError]) throws {
|
|
if !validationErrors.isEmpty {
|
|
throw SaveError.validation(validationErrors)
|
|
}
|
|
|
|
if url == nil {
|
|
let existingInfo = server.info.connection
|
|
let other: ConnectionInfo.URLType = urlType == .internal ? .external : .internal
|
|
if existingInfo.address(for: other) == nil,
|
|
useCloud == false || (useCloud == nil && !existingInfo.useCloud) {
|
|
throw SaveError.lastURL
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc private func cancel() {
|
|
onDismissCallback?(self)
|
|
}
|
|
|
|
@objc private func save() {
|
|
let givenURL = (form.rowBy(tag: RowTag.url.rawValue) as? URLRow)?.value
|
|
let useCloud = (form.rowBy(tag: RowTag.useCloud.rawValue) as? SwitchRow)?.value
|
|
let localPush = (form.rowBy(tag: RowTag.localPush.rawValue) as? SwitchRow)?.value
|
|
|
|
func commit() {
|
|
server.update { info in
|
|
info.connection.set(address: givenURL, for: urlType)
|
|
|
|
if let useCloud {
|
|
info.connection.useCloud = useCloud
|
|
}
|
|
|
|
if let localPush {
|
|
info.connection.isLocalPushEnabled = localPush
|
|
}
|
|
|
|
if let section = form.sectionBy(tag: RowTag.ssids.rawValue) as? MultivaluedSection {
|
|
info.connection.internalSSIDs = section.allRows
|
|
.compactMap { $0 as? TextRow }
|
|
.compactMap(\.value)
|
|
.filter { !$0.isEmpty }
|
|
}
|
|
|
|
if let section = form.sectionBy(tag: RowTag.hardwareAddresses.rawValue) as? MultivaluedSection {
|
|
info.connection.internalHardwareAddresses = section.allRows
|
|
.compactMap { $0 as? TextRow }
|
|
.compactMap(\.value)
|
|
.map { $0.lowercased() }
|
|
.filter { !$0.isEmpty }
|
|
}
|
|
}
|
|
|
|
onDismissCallback?(self)
|
|
}
|
|
|
|
updateNavigationItems(isChecking: true)
|
|
|
|
firstly { () -> Promise<Void> in
|
|
try check(url: givenURL, useCloud: useCloud, validationErrors: form.validate())
|
|
|
|
if useCloud == true, let url = server.info.connection.address(for: .remoteUI) {
|
|
return Current.webhooks.sendTest(server: server, baseURL: url)
|
|
}
|
|
|
|
if let givenURL, useCloud != true {
|
|
return Current.webhooks.sendTest(server: server, baseURL: givenURL)
|
|
}
|
|
|
|
return .value(())
|
|
}.ensure {
|
|
self.updateNavigationItems(isChecking: false)
|
|
}.done {
|
|
commit()
|
|
}.catch { error in
|
|
let alert = UIAlertController(
|
|
title: L10n.Settings.ConnectionSection.ValidateError.title,
|
|
message: error.localizedDescription,
|
|
preferredStyle: .alert
|
|
)
|
|
|
|
let canCommit: Bool
|
|
|
|
if let error = error as? SaveError {
|
|
canCommit = !error.isFinal
|
|
} else {
|
|
canCommit = true
|
|
}
|
|
|
|
if canCommit {
|
|
alert.addAction(UIAlertAction(
|
|
title: L10n.Settings.ConnectionSection.ValidateError.useAnyway,
|
|
style: .default,
|
|
handler: { _ in commit() }
|
|
))
|
|
}
|
|
|
|
alert.addAction(UIAlertAction(
|
|
title: L10n.Settings.ConnectionSection.ValidateError.editUrl,
|
|
style: .cancel,
|
|
handler: nil
|
|
))
|
|
self.present(alert, animated: true, completion: nil)
|
|
}
|
|
}
|
|
|
|
fileprivate enum RowTag: String {
|
|
case url
|
|
case internalURLWarning
|
|
case ssids
|
|
case hardwareAddresses
|
|
case useCloud
|
|
case localPush
|
|
}
|
|
|
|
private func updateNavigationItems(isChecking: Bool) {
|
|
navigationItem.leftBarButtonItems = [
|
|
UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancel)),
|
|
]
|
|
|
|
if isChecking {
|
|
let activityIndicator: UIActivityIndicatorView
|
|
|
|
activityIndicator = .init(style: .medium)
|
|
|
|
activityIndicator.startAnimating()
|
|
|
|
navigationItem.rightBarButtonItems = [
|
|
UIBarButtonItem(customView: activityIndicator),
|
|
]
|
|
} else {
|
|
navigationItem.rightBarButtonItems = [
|
|
UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(save)),
|
|
]
|
|
}
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
updateNavigationItems(isChecking: false)
|
|
|
|
if urlType.isAffectedByCloud, server.info.connection.canUseCloud {
|
|
form +++ SwitchRow {
|
|
$0.title = L10n.Settings.ConnectionSection.HomeAssistantCloud.title
|
|
$0.tag = RowTag.useCloud.rawValue
|
|
$0.value = server.info.connection.useCloud
|
|
}
|
|
}
|
|
|
|
form +++ Section()
|
|
<<< URLRow(RowTag.url.rawValue) {
|
|
$0.value = server.info.connection.address(for: urlType)
|
|
$0.hidden = .function([RowTag.useCloud.rawValue], { form in
|
|
if let row = form.rowBy(tag: RowTag.useCloud.rawValue) as? SwitchRow {
|
|
// if cloud's around, hide when it's turned on
|
|
return row.value == true
|
|
} else {
|
|
// never hide if cloud isn't around
|
|
return false
|
|
}
|
|
})
|
|
$0.placeholder = { () -> String? in
|
|
switch urlType {
|
|
case .internal: return L10n.Settings.ConnectionSection.InternalBaseUrl.placeholder
|
|
case .external: return L10n.Settings.ConnectionSection.ExternalBaseUrl.placeholder
|
|
case .remoteUI, .none: return nil
|
|
}
|
|
}()
|
|
}
|
|
|
|
<<< InfoLabelRow {
|
|
$0.title = L10n.Settings.ConnectionSection.cloudOverridesExternal
|
|
$0.hidden = .function([RowTag.useCloud.rawValue], { form in
|
|
if let row = form.rowBy(tag: RowTag.useCloud.rawValue) as? SwitchRow {
|
|
// this is effectively the visual replacement for the external url, so show when cloud is on
|
|
return row.value == false
|
|
} else {
|
|
// always hide if we're not offering the cloud option
|
|
return true
|
|
}
|
|
})
|
|
}
|
|
<<< InfoLabelRow {
|
|
$0.tag = RowTag.internalURLWarning.rawValue
|
|
if server.info.connection.internalSSIDs?.isEmpty ?? true,
|
|
server.info.connection.internalHardwareAddresses?.isEmpty ?? true {
|
|
#if targetEnvironment(macCatalyst)
|
|
$0.title = "‼️" + L10n.Settings.ConnectionSection.InternalBaseUrl.SsidBssidRequired.title
|
|
#else
|
|
$0.title = "‼️" + L10n.Settings.ConnectionSection.InternalBaseUrl.SsidRequired.title
|
|
|
|
#endif
|
|
} else {
|
|
$0.title = L10n.Settings.ConnectionSection.InternalBaseUrl.SsidRequired.title
|
|
}
|
|
}
|
|
|
|
if urlType.isAffectedBySSID {
|
|
form +++ locationPermissionSection()
|
|
|
|
form +++ MultivaluedSection(
|
|
tag: .ssids,
|
|
header: L10n.Settings.ConnectionSection.InternalUrlSsids.header,
|
|
footer: L10n.Settings.ConnectionSection.InternalUrlSsids.footer,
|
|
addNewLabel: L10n.Settings.ConnectionSection.InternalUrlSsids.addNewSsid,
|
|
placeholder: L10n.Settings.ConnectionSection.InternalUrlSsids.placeholder,
|
|
currentValue: Current.connectivity.currentWiFiSSID,
|
|
existingValues: server.info.connection.internalSSIDs ?? [],
|
|
valueRules: RuleSet<String>()
|
|
)
|
|
}
|
|
|
|
if urlType.isAffectedByHardwareAddress {
|
|
var rules = RuleSet<String>()
|
|
rules.add(rule: RuleRegExp(
|
|
regExpr: "^[a-zA-Z0-9]{2}\\:[a-zA-Z0-9]{2}\\:[a-zA-Z0-9]{2}\\:[a-zA-Z0-9]{2}\\:[a-zA-Z0-9]{2}\\:[a-zA-Z0-9]{2}$",
|
|
allowsEmpty: true,
|
|
msg: L10n.Settings.ConnectionSection.InternalUrlHardwareAddresses.invalid,
|
|
id: nil
|
|
))
|
|
|
|
form +++ MultivaluedSection(
|
|
tag: .hardwareAddresses,
|
|
header: L10n.Settings.ConnectionSection.InternalUrlHardwareAddresses.header,
|
|
footer: L10n.Settings.ConnectionSection.InternalUrlHardwareAddresses.footer,
|
|
addNewLabel: L10n.Settings.ConnectionSection.InternalUrlHardwareAddresses.addNewSsid,
|
|
placeholder: "aa:bb:cc:dd:ee:ff",
|
|
currentValue: Current.connectivity.currentNetworkHardwareAddress,
|
|
existingValues: server.info.connection.internalHardwareAddresses ?? [],
|
|
valueRules: rules
|
|
)
|
|
}
|
|
|
|
if urlType.hasLocalPush {
|
|
form +++ Section(
|
|
footer: L10n.Settings.ConnectionSection.localPushDescription
|
|
) <<< SwitchRow(RowTag.localPush.rawValue) {
|
|
$0.title = L10n.SettingsDetails.Notifications.LocalPush.title
|
|
$0.value = server.info.connection.isLocalPushEnabled
|
|
} <<< LearnMoreButtonRow {
|
|
$0.onCellSelection { cell, _ in
|
|
openURLInBrowser(
|
|
URL(string: "https://companion.home-assistant.io/app/ios/local-push")!,
|
|
cell.formViewController()
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func locationPermissionSection() -> Section {
|
|
class PermissionWatchingDelegate: NSObject, CLLocationManagerDelegate {
|
|
let section: Section
|
|
|
|
init(section: Section) {
|
|
self.section = section
|
|
}
|
|
|
|
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
|
section.evaluateHidden()
|
|
}
|
|
|
|
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
|
|
section.evaluateHidden()
|
|
}
|
|
}
|
|
|
|
let section = Section()
|
|
var locationManager: CLLocationManager? = CLLocationManager()
|
|
var permissionDelegate: PermissionWatchingDelegate? = PermissionWatchingDelegate(section: section)
|
|
locationManager?.delegate = permissionDelegate
|
|
|
|
section.hidden = .function([], { _ in
|
|
if let locationManager {
|
|
return locationManager.authorizationStatus == .authorizedAlways &&
|
|
locationManager.accuracyAuthorization == .fullAccuracy
|
|
} else {
|
|
return locationManager?.authorizationStatus == .authorizedAlways
|
|
}
|
|
})
|
|
section.evaluateHidden()
|
|
|
|
after(life: self).done {
|
|
// we're keeping these lifetimes around longer so they update
|
|
locationManager = nil
|
|
permissionDelegate = nil
|
|
}
|
|
|
|
section <<< InfoLabelRow {
|
|
$0.title = L10n.Settings.ConnectionSection.ssidPermissionAndAccuracyMessage
|
|
|
|
$0.displayType = .important
|
|
|
|
$0.cellUpdate { cell, _ in
|
|
cell.accessibilityTraits.insert(.button)
|
|
cell.selectionStyle = .default
|
|
}
|
|
|
|
$0.onCellSelection { _, _ in
|
|
if locationManager?.authorizationStatus == .notDetermined {
|
|
locationManager?.requestAlwaysAuthorization()
|
|
} else {
|
|
UIApplication.shared.openSettings(destination: .location)
|
|
}
|
|
}
|
|
}
|
|
|
|
return section
|
|
}
|
|
}
|
|
|
|
private extension MultivaluedSection {
|
|
convenience init(
|
|
tag: ConnectionURLViewController.RowTag,
|
|
header: String,
|
|
footer: String,
|
|
addNewLabel: String,
|
|
placeholder: String,
|
|
currentValue: @escaping () -> String?,
|
|
existingValues: [String],
|
|
valueRules: RuleSet<String>
|
|
) {
|
|
self.init(
|
|
multivaluedOptions: [.Insert, .Delete],
|
|
header: header,
|
|
footer: footer
|
|
) { section in
|
|
section.tag = tag.rawValue
|
|
section.addButtonProvider = { _ in
|
|
ButtonRow {
|
|
$0.title = addNewLabel
|
|
}.cellUpdate { cell, _ in
|
|
cell.textLabel?.textAlignment = .natural
|
|
cell.selectionStyle = .default
|
|
}
|
|
}
|
|
|
|
func row(for value: String?) -> TextRow {
|
|
TextRow {
|
|
$0.placeholder = placeholder
|
|
$0.value = value
|
|
$0.add(ruleSet: valueRules)
|
|
}
|
|
}
|
|
|
|
section.multivaluedRowToInsertAt = { _ in
|
|
let current = currentValue()
|
|
|
|
if section.allRows.contains(where: { ($0 as? TextRow)?.value == current }) {
|
|
return row(for: nil)
|
|
} else {
|
|
return row(for: current)
|
|
}
|
|
}
|
|
|
|
section.append(contentsOf: existingValues.map { row(for: $0) })
|
|
}
|
|
}
|
|
}
|