mirror of
https://github.com/home-assistant/iOS.git
synced 2026-04-12 15:26:45 -05:00
394 lines
17 KiB
Swift
394 lines
17 KiB
Swift
import Foundation
|
|
import KeychainAccess
|
|
import UIKit
|
|
import Version
|
|
|
|
/// Contains shared constants
|
|
public enum AppConstants {
|
|
public enum WebURLs {
|
|
public static var homeAssistant = URL(string: "https://www.home-assistant.io")!
|
|
public static var homeAssistantGetStarted = URL(string: "https://www.home-assistant.io/installation/")!
|
|
public static var homeAssistantCompanionGetStarted =
|
|
URL(string: "https://companion.home-assistant.io/docs/getting_started/")!
|
|
public static var companionAppDocs = URL(string: "https://companion.home-assistant.io")!
|
|
public static var companionAppDocsTroubleshooting =
|
|
URL(string: "https://companion.home-assistant.io/docs/troubleshooting/errors")!
|
|
public static var beta = URL(string: "https://companion.home-assistant.io/app/ios/beta")!
|
|
public static var betaMac = URL(string: "https://companion.home-assistant.io/app/ios/beta_mac")!
|
|
public static var review = URL(string: "https://companion.home-assistant.io/app/ios/review")!
|
|
public static var reviewMac = URL(string: "https://companion.home-assistant.io/app/ios/review_mac")!
|
|
public static var translate = URL(string: "https://companion.home-assistant.io/app/ios/translate")!
|
|
public static var forums = URL(string: "https://community.home-assistant.io/")!
|
|
public static var chat = URL(string: "https://companion.home-assistant.io/app/ios/chat")!
|
|
public static var twitter = URL(string: "https://twitter.com/home_assistant")!
|
|
public static var facebook = URL(string: "https://www.facebook.com/292963007723872")!
|
|
public static var repo = URL(string: "https://companion.home-assistant.io/app/ios/repo")!
|
|
public static var issues = URL(string: "https://companion.home-assistant.io/app/ios/issues")!
|
|
public static var companionAppConnectionSecurityLevel =
|
|
URL(string: "https://companion.home-assistant.io/docs/getting_started/connection-security-level")!
|
|
public static var companionLocalPush =
|
|
URL(string: "https://companion.home-assistant.io/app/ios/local-push")!
|
|
public static var nfcDocs =
|
|
URL(string: "https://companion.home-assistant.io/app/ios/nfc")!
|
|
}
|
|
|
|
public enum QueryItems: String, CaseIterable {
|
|
case openMoreInfoDialog = "more-info-entity-id"
|
|
case isComingFromAppIntent = "isComingFromAppIntent"
|
|
}
|
|
|
|
public enum WebRTC {
|
|
public static let iceServers = [
|
|
"stun:stun.home-assistant.io:80",
|
|
"stun:stun.home-assistant.io:3478",
|
|
]
|
|
}
|
|
|
|
/// Home Assistant Blue
|
|
public static var tintColor: UIColor {
|
|
#if os(iOS)
|
|
return UIColor { [lighterTintColor, darkerTintColor] (traitCollection: UITraitCollection) -> UIColor in
|
|
traitCollection.userInterfaceStyle == .dark ? lighterTintColor : darkerTintColor
|
|
}
|
|
#else
|
|
return lighterTintColor
|
|
#endif
|
|
}
|
|
|
|
public static var lighterTintColor: UIColor {
|
|
UIColor(hue: 199.0 / 360.0, saturation: 0.99, brightness: 0.96, alpha: 1.0)
|
|
}
|
|
|
|
public static var darkerTintColor: UIColor {
|
|
UIColor(hue: 199.0 / 360.0, saturation: 0.99, brightness: 0.67, alpha: 1.0)
|
|
}
|
|
|
|
/// Help icon UIBarButtonItem
|
|
#if os(iOS)
|
|
public static var helpBarButtonItem: UIBarButtonItem {
|
|
with(UIBarButtonItem(
|
|
icon: .helpCircleOutlineIcon,
|
|
target: nil,
|
|
action: nil
|
|
)) {
|
|
$0.accessibilityLabel = L10n.helpLabel
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/// The Bundle ID used for the AppGroupID
|
|
public static var BundleID: String {
|
|
let baseBundleID = Bundle.main.bundleIdentifier!
|
|
var removeBundleSuffix = baseBundleID.replacingOccurrences(of: ".APNSAttachmentService", with: "")
|
|
removeBundleSuffix = removeBundleSuffix.replacingOccurrences(of: ".Intents", with: "")
|
|
removeBundleSuffix = removeBundleSuffix.replacingOccurrences(of: ".NotificationContentExtension", with: "")
|
|
removeBundleSuffix = removeBundleSuffix.replacingOccurrences(of: ".TodayWidget", with: "")
|
|
removeBundleSuffix = removeBundleSuffix.replacingOccurrences(of: ".watchkitapp.watchkitextension", with: "")
|
|
removeBundleSuffix = removeBundleSuffix.replacingOccurrences(of: ".watchkitapp", with: "")
|
|
removeBundleSuffix = removeBundleSuffix.replacingOccurrences(of: ".Widgets", with: "")
|
|
removeBundleSuffix = removeBundleSuffix.replacingOccurrences(of: ".ShareExtension", with: "")
|
|
removeBundleSuffix = removeBundleSuffix.replacingOccurrences(of: ".PushProvider", with: "")
|
|
removeBundleSuffix = removeBundleSuffix.replacingOccurrences(of: ".Matter", with: "")
|
|
|
|
return removeBundleSuffix
|
|
}
|
|
|
|
public static var deeplinkURL: URL {
|
|
switch Current.appConfiguration {
|
|
case .debug:
|
|
return URL(string: "homeassistant-dev://")!
|
|
case .beta:
|
|
return URL(string: "homeassistant-beta://")!
|
|
default:
|
|
return URL(string: "homeassistant://")!
|
|
}
|
|
}
|
|
|
|
public static func invitationURL(serverURL: URL) -> URL? {
|
|
guard let encodedURLString = serverURL.absoluteString
|
|
.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
|
|
return nil
|
|
}
|
|
return URL(string: "https://my.home-assistant.io/invite/#url=\(encodedURLString)")
|
|
}
|
|
|
|
public static func navigateDeeplinkURL(
|
|
path: String,
|
|
serverId: String,
|
|
queryParams: String? = nil,
|
|
avoidUnnecessaryReload: Bool
|
|
) -> URL? {
|
|
var url = URL(
|
|
string: "\(AppConstants.deeplinkURL.absoluteString)navigate/\(path)?server=\(serverId)&avoidUnnecessaryReload=\(avoidUnnecessaryReload)&\(AppConstants.QueryItems.isComingFromAppIntent.rawValue)=true"
|
|
)
|
|
|
|
if let queryParams, let newURL = URL(string: "\(url?.absoluteString ?? "")&\(queryParams)") {
|
|
url = newURL
|
|
}
|
|
|
|
return url
|
|
}
|
|
|
|
public static func openPageDeeplinkURL(path: String, serverId: String) -> URL? {
|
|
AppConstants.navigateDeeplinkURL(path: path, serverId: serverId, avoidUnnecessaryReload: true)?
|
|
.withWidgetAuthenticity()
|
|
}
|
|
|
|
public static func openEntityDeeplinkURL(entityId: String, serverId: String) -> URL? {
|
|
AppConstants.navigateDeeplinkURL(
|
|
path: "",
|
|
serverId: serverId,
|
|
queryParams: "\(AppConstants.QueryItems.openMoreInfoDialog.rawValue)=\(entityId)",
|
|
avoidUnnecessaryReload: true
|
|
)?.withWidgetAuthenticity()
|
|
}
|
|
|
|
public static func openCameraDeeplinkURL(entityId: String, serverId: String) -> URL? {
|
|
URL(
|
|
string: "\(AppConstants.deeplinkURL.absoluteString)camera/?entityId=\(entityId)&serverId=\(serverId)&\(AppConstants.QueryItems.isComingFromAppIntent.rawValue)=true"
|
|
)
|
|
}
|
|
|
|
public static func openCameraListDeeplinkURL(serverId: String? = nil) -> URL? {
|
|
var urlString = "\(AppConstants.deeplinkURL.absoluteString)camera/?"
|
|
if let serverId {
|
|
urlString += "serverId=\(serverId)&"
|
|
}
|
|
urlString += "\(AppConstants.QueryItems.isComingFromAppIntent.rawValue)=true"
|
|
return URL(string: urlString)
|
|
}
|
|
|
|
@available(iOS 16.0, watchOS 9.0, *)
|
|
public static func todoListAddItemURL(listId: String, serverId: String) -> URL? {
|
|
guard !serverId.isEmpty, !listId.isEmpty else {
|
|
return nil
|
|
}
|
|
return URL(string: "\(AppConstants.deeplinkURL.absoluteString)navigate/todo")?.appending(queryItems: [
|
|
URLQueryItem(name: "entity_id", value: listId),
|
|
URLQueryItem(name: "serverId", value: serverId),
|
|
URLQueryItem(name: "add_item", value: "true"),
|
|
])
|
|
}
|
|
|
|
@available(iOS 16.0, watchOS 9.0, *)
|
|
public static func todoListOpenURL(listId: String, serverId: String) -> URL? {
|
|
guard !serverId.isEmpty, !listId.isEmpty else {
|
|
return nil
|
|
}
|
|
return URL(string: "\(AppConstants.deeplinkURL.absoluteString)navigate/todo")?.appending(queryItems: [
|
|
URLQueryItem(name: "entity_id", value: listId),
|
|
URLQueryItem(name: "serverId", value: serverId),
|
|
])
|
|
}
|
|
|
|
public static func assistDeeplinkURL(serverId: String, pipelineId: String, startListening: Bool) -> URL? {
|
|
URL(
|
|
string: "\(AppConstants.deeplinkURL.absoluteString)assist?serverId=\(serverId)&pipelineId=\(pipelineId)&startListening=\(startListening)"
|
|
)?.withWidgetAuthenticity()
|
|
}
|
|
|
|
public static var createCustomWidgetURL: URL {
|
|
URL(string: "\(AppConstants.deeplinkURL.absoluteString)createCustomWidget")!
|
|
}
|
|
|
|
/// The App Group ID used by the app and extensions for sharing data.
|
|
public static var AppGroupID: String {
|
|
"group." + BundleID.lowercased()
|
|
}
|
|
|
|
public static var AppGroupContainer: URL {
|
|
let fileManager = FileManager.default
|
|
|
|
let groupDir = fileManager.containerURL(forSecurityApplicationGroupIdentifier: AppConstants.AppGroupID)
|
|
|
|
guard let groupDir else {
|
|
fatalError("Unable to get groupDir.")
|
|
}
|
|
|
|
return groupDir
|
|
}
|
|
|
|
public static var appGRDBFile: URL {
|
|
let fileManager = FileManager.default
|
|
let directoryURL = Self.AppGroupContainer.appendingPathComponent("databases", isDirectory: true)
|
|
if !fileManager.fileExists(atPath: directoryURL.path) {
|
|
do {
|
|
try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true)
|
|
} catch {
|
|
Current.Log.error("Failed to create App GRDB file")
|
|
}
|
|
}
|
|
let databaseURL = directoryURL.appendingPathComponent("App.sqlite")
|
|
return databaseURL
|
|
}
|
|
|
|
public static var clientEventsFile: URL {
|
|
let fileManager = FileManager.default
|
|
let directoryURL = Self.AppGroupContainer.appendingPathComponent("databases", isDirectory: true)
|
|
if !fileManager.fileExists(atPath: directoryURL.path) {
|
|
do {
|
|
try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true)
|
|
} catch {
|
|
Current.Log.error("Failed to create Client Events file")
|
|
}
|
|
}
|
|
let eventsURL = directoryURL.appendingPathComponent("clientEvents.json")
|
|
return eventsURL
|
|
}
|
|
|
|
public static var widgetsCacheURL: URL = {
|
|
let fileManager = FileManager.default
|
|
let directoryURL = Self.AppGroupContainer.appendingPathComponent("caches/widgets", isDirectory: true)
|
|
return directoryURL
|
|
}()
|
|
|
|
public static func widgetCachedStates(widgetId: String) -> URL {
|
|
let fileManager = FileManager.default
|
|
let directoryURL = Self.widgetsCacheURL
|
|
if !fileManager.fileExists(atPath: directoryURL.path) {
|
|
do {
|
|
try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true)
|
|
} catch {
|
|
Current.Log.error("Failed to create Client Events file")
|
|
}
|
|
}
|
|
let eventsURL = directoryURL.appendingPathComponent("/widgetId-\(widgetId).json")
|
|
return eventsURL
|
|
}
|
|
|
|
public static var watchMagicItemsInfo: URL {
|
|
let fileManager = FileManager.default
|
|
let directoryURL = Self.AppGroupContainer.appendingPathComponent("caches", isDirectory: true)
|
|
if !fileManager.fileExists(atPath: directoryURL.path) {
|
|
do {
|
|
try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true)
|
|
} catch {
|
|
Current.Log.error("Failed to magic items info file")
|
|
}
|
|
}
|
|
let eventsURL = directoryURL.appendingPathComponent("magicItemsInfo.json")
|
|
return eventsURL
|
|
}
|
|
|
|
public static var LogsDirectory: URL {
|
|
let fileManager = FileManager.default
|
|
let directoryURL = AppGroupContainer.appendingPathComponent("logs", isDirectory: true)
|
|
|
|
if !fileManager.fileExists(atPath: directoryURL.path) {
|
|
do {
|
|
try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)
|
|
} catch {
|
|
fatalError("Error while attempting to create data store URL: \(error)")
|
|
}
|
|
}
|
|
|
|
return directoryURL
|
|
}
|
|
|
|
public static var DownloadsDirectory: URL {
|
|
var directoryURL: URL = FileManager.default.urls(for: .cachesDirectory, in: .allDomainsMask).first!
|
|
|
|
// Save directly in macOS Downloads folder if running on Catalyst and allowed access to download folder when
|
|
// prompted.
|
|
if Current.isCatalyst, let macDownloadFolder = FileManager.default.urls(
|
|
for: .downloadsDirectory,
|
|
in: .userDomainMask
|
|
).first {
|
|
directoryURL = macDownloadFolder
|
|
} else {
|
|
directoryURL = directoryURL.appendingPathComponent(
|
|
"Downloads",
|
|
isDirectory: true
|
|
)
|
|
}
|
|
if !FileManager.default.fileExists(atPath: directoryURL.path) {
|
|
do {
|
|
try FileManager.default.createDirectory(
|
|
at: directoryURL,
|
|
withIntermediateDirectories: true,
|
|
attributes: nil
|
|
)
|
|
} catch {
|
|
fatalError("Error while attempting to create downloads path URL: \(error)")
|
|
}
|
|
}
|
|
|
|
return directoryURL
|
|
}
|
|
|
|
/// An initialized Keychain from KeychainAccess.
|
|
public static var Keychain: KeychainAccess.Keychain {
|
|
KeychainAccess.Keychain(service: BundleID)
|
|
}
|
|
|
|
/// A permanent ID stored in UserDefaults and Keychain.
|
|
public static var PermanentID: String {
|
|
let storageKey = "deviceUID"
|
|
let defaultsStore = UserDefaults(suiteName: AppConstants.AppGroupID)
|
|
let keychain = KeychainAccess.Keychain(service: storageKey)
|
|
|
|
if let keychainUID = keychain[storageKey] {
|
|
return keychainUID
|
|
}
|
|
|
|
if let userDefaultsUID = defaultsStore?.object(forKey: storageKey) as? String {
|
|
return userDefaultsUID
|
|
}
|
|
|
|
let newID = UUID().uuidString
|
|
|
|
if keychain[storageKey] == nil {
|
|
keychain[storageKey] = newID
|
|
}
|
|
|
|
if defaultsStore?.object(forKey: storageKey) == nil {
|
|
defaultsStore?.setValue(newID, forKey: storageKey)
|
|
}
|
|
|
|
return newID
|
|
}
|
|
|
|
public static var build: String {
|
|
SharedPlistFiles.Info.cfBundleVersion
|
|
}
|
|
|
|
public static var version: String {
|
|
SharedPlistFiles.Info.cfBundleShortVersionString
|
|
}
|
|
|
|
static var clientVersion: Version {
|
|
// swiftlint:disable:next force_try
|
|
var clientVersion = try! Version(version)
|
|
clientVersion.build = build
|
|
return clientVersion
|
|
}
|
|
}
|
|
|
|
public extension Version {
|
|
static let canSendDeviceID: Version = .init(minor: 104)
|
|
static let pedometerIconsAvailable: Version = .init(minor: 105)
|
|
static let tagWebhookAvailable: Version = .init(minor: 114, prerelease: "b5")
|
|
static let actionSyncing: Version = .init(minor: 115, prerelease: "any0")
|
|
static let localPushConfirm: Version = .init(major: 2021, minor: 10, prerelease: "any0")
|
|
static let externalBusCommandRestart: Version = .init(major: 2021, minor: 12, prerelease: "b6")
|
|
static let updateLocationGPSOptional: Version = .init(major: 2022, minor: 2, prerelease: "any0")
|
|
static let fullWebhookSecretKey: Version = .init(major: 2022, minor: 3)
|
|
static let conversationWebhook: Version = .init(major: 2023, minor: 2, prerelease: "any0")
|
|
static let externalBusCommandSidebar: Version = .init(major: 2023, minor: 4, prerelease: "b3")
|
|
static let externalBusCommandAutomationEditor: Version = .init(major: 2024, minor: 2, prerelease: "any0")
|
|
static let canUseAppThemeForStatusBar: Version = .init(major: 2024, minor: 7)
|
|
/// The version where the app can subscribe to entities changes with a filter (e.g. only state changes from sensor
|
|
/// domain)
|
|
static let canSubscribeEntitiesChangesWithFilter: Version = .init(major: 2024, minor: 10)
|
|
/// Allows app to ask frontend to navigate to a specific page
|
|
static let canNavigateThroughFrontend: Version = .init(major: 2025, minor: 6, prerelease: "any0")
|
|
/// Allows app to ask frontend to navigate to a more info dialog
|
|
static let canNavigateMoreInfoDialogThroughFrontend: Version = .init(major: 2026, minor: 1, prerelease: "any0")
|
|
/// Frontend introduces the quickbar with Ctrl+K keyboard shortcut in 2026.2
|
|
static let quickSearchKeyboardShortcut: Version = .init(major: 2026, minor: 2, prerelease: "any0")
|
|
|
|
var coreRequiredString: String {
|
|
L10n.requiresVersion(String(format: "core-%d.%d", major, minor ?? -1))
|
|
}
|
|
}
|