mirror of
https://github.com/home-assistant/iOS.git
synced 2026-02-16 10:21:16 -06:00
<!-- Thank you for submitting a Pull Request and helping to improve Home Assistant. Please complete the following sections to help the processing and review of your changes. Please do not delete anything from this template. --> ## Summary <!-- Provide a brief summary of the changes you have made and most importantly what they aim to achieve --> ## Screenshots <!-- If this is a user-facing change not in the frontend, please include screenshots in light and dark mode. --> ## Link to pull request in Documentation repository <!-- Pull requests that add, change or remove functionality must have a corresponding pull request in the Companion App Documentation repository (https://github.com/home-assistant/companion.home-assistant). Please add the number of this pull request after the "#" --> Documentation: home-assistant/companion.home-assistant# ## Any other notes <!-- If there is any other information of note, like if this Pull Request is part of a bigger change, please include it here. -->
238 lines
7.9 KiB
Swift
238 lines
7.9 KiB
Swift
import Communicator
|
|
import Foundation
|
|
import NetworkExtension
|
|
import PromiseKit
|
|
import Shared
|
|
|
|
enum WatchHomeType {
|
|
case undefined
|
|
case empty
|
|
case config(watchConfig: WatchConfig, magicItemsInfo: [MagicItem.Info])
|
|
case error(message: String)
|
|
}
|
|
|
|
final class WatchHomeViewModel: ObservableObject {
|
|
@Published var isLoading = false
|
|
@Published var showAssist = false
|
|
@Published var showError = false
|
|
@Published var errorMessage = ""
|
|
@Published var currentSSID: String = ""
|
|
@Published private(set) var homeType: WatchHomeType = .undefined
|
|
|
|
@Published var watchConfig: WatchConfig = .init()
|
|
@Published var magicItemsInfo: [MagicItem.Info] = []
|
|
|
|
// If the watchConfig items are the same but it's customization properties
|
|
// are different, the list won't refresh. This is a workaround to force a refresh
|
|
@Published var refreshListID: UUID = .init()
|
|
|
|
@MainActor
|
|
func fetchNetworkInfo() async {
|
|
let networkInformation = await Current.networkInformation
|
|
WatchUserDefaults.shared.set(networkInformation?.ssid, key: .watchSSID)
|
|
currentSSID = networkInformation?.ssid ?? ""
|
|
}
|
|
|
|
@MainActor
|
|
func initialRoutine() {
|
|
// First display whatever is in cache
|
|
loadCache()
|
|
// Now fetch new data in the background (shows loading indicator only for this fetch)
|
|
isLoading = true
|
|
requestConfig()
|
|
}
|
|
|
|
@MainActor
|
|
func requestConfig() {
|
|
homeType = .undefined
|
|
guard Communicator.shared.currentReachability != .notReachable else {
|
|
Current.Log.error("iPhone reachability is not immediate reachable")
|
|
loadCache()
|
|
return
|
|
}
|
|
isLoading = true
|
|
Communicator.shared.send(.init(
|
|
identifier: InteractiveImmediateMessages.watchConfig.rawValue,
|
|
reply: { [weak self] message in
|
|
self?.handleMessageResponse(message)
|
|
}
|
|
))
|
|
}
|
|
|
|
func info(for magicItem: MagicItem) -> MagicItem.Info {
|
|
magicItemsInfo.first(where: {
|
|
$0.id == magicItem.serverUniqueId
|
|
}) ?? .init(
|
|
id: magicItem.id,
|
|
name: magicItem.id,
|
|
iconName: ""
|
|
)
|
|
}
|
|
|
|
@MainActor
|
|
private func handleMessageResponse(_ message: ImmediateMessage) {
|
|
switch message.identifier {
|
|
case InteractiveImmediateResponses.emptyWatchConfigResponse.rawValue:
|
|
clearCacheAndLoad()
|
|
case InteractiveImmediateResponses.watchConfigResponse.rawValue:
|
|
setupConfig(message)
|
|
default:
|
|
Current.Log
|
|
.error("Received unmapped response id for watch config request, id: \(message.identifier)")
|
|
loadCache()
|
|
}
|
|
updateLoading(isLoading: false)
|
|
}
|
|
|
|
@MainActor
|
|
private func setupConfig(_ message: ImmediateMessage) {
|
|
guard let configData = message.content["config"] as? Data,
|
|
let watchConfig = WatchConfig.decodeForWatch(configData) else {
|
|
Current.Log.error("Failed to get config data from watch config response")
|
|
return
|
|
}
|
|
|
|
guard let magicItemsInfo = message.content["magicItemsInfo"] as? [Data] else {
|
|
Current.Log.error("Failed to get magicItemsInfo data array from watch config response")
|
|
return
|
|
}
|
|
let itemsInfo = magicItemsInfo.map({ MagicItem.Info.decodeForWatch($0) })
|
|
|
|
do {
|
|
try Current.database().write { db in
|
|
try watchConfig.insert(db, onConflict: .replace)
|
|
}
|
|
saveItemsInfoInCache(itemsInfo.compactMap({ $0 }))
|
|
} catch {
|
|
Current.Log
|
|
.error(
|
|
"Failed to save watch config and/or magic item info in database on Apple watch, error: \(error.localizedDescription)"
|
|
)
|
|
}
|
|
|
|
loadCache()
|
|
}
|
|
|
|
@MainActor
|
|
func loadCache() {
|
|
do {
|
|
if let watchConfig = try Current.database().read({ db in
|
|
try WatchConfig.fetchOne(db)
|
|
}) {
|
|
loadInformationCache(watchConfig: watchConfig)
|
|
} else {
|
|
updateConfig(config: .init(), magicItemsInfo: [])
|
|
}
|
|
} catch {
|
|
Current.Log.error("Failed to fetch watch config from database, error: \(error.localizedDescription)")
|
|
displayError(message: L10n.Watch.Config.Cache.Error.message)
|
|
updateConfig(config: .init(), magicItemsInfo: [])
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private func loadInformationCache(watchConfig: WatchConfig) {
|
|
let magicItemsInfo = getItemsInfoFromCache()
|
|
if !magicItemsInfo.isEmpty {
|
|
updateConfig(config: watchConfig, magicItemsInfo: magicItemsInfo)
|
|
resetError()
|
|
} else {
|
|
Current.Log.error("Failed to retrieve magic items cache")
|
|
displayError(message: L10n.Watch.Config.Error.message("No information cached"))
|
|
}
|
|
updateLoading(isLoading: false)
|
|
}
|
|
|
|
@MainActor
|
|
private func clearCacheAndLoad() {
|
|
do {
|
|
_ = try Current.database().write { db in
|
|
try WatchConfig.deleteAll(db)
|
|
}
|
|
} catch {
|
|
Current.Log
|
|
.error(
|
|
"Failed to delete watch config and/or magic item info in database on Apple watch, error: \(error.localizedDescription)"
|
|
)
|
|
}
|
|
|
|
deleteItemsInfoInCache()
|
|
loadCache()
|
|
}
|
|
|
|
private func saveItemsInfoInCache(_ itemsInfo: [MagicItem.Info]) {
|
|
do {
|
|
let fileURL = AppConstants.watchMagicItemsInfo
|
|
let jsonData = try JSONEncoder().encode(itemsInfo)
|
|
try jsonData.write(to: fileURL)
|
|
Current.Log
|
|
.verbose("JSON saved successfully for watch magic items info, file URL: \(fileURL.absoluteString)")
|
|
} catch {
|
|
Current.Log.error("Error saving JSON for magic items info: \(error)")
|
|
}
|
|
}
|
|
|
|
private func deleteItemsInfoInCache() {
|
|
do {
|
|
let fileURL = AppConstants.watchMagicItemsInfo
|
|
try FileManager.default.removeItem(at: fileURL)
|
|
} catch {
|
|
Current.Log.error("Error deleting JSON for magic items info: \(error)")
|
|
}
|
|
}
|
|
|
|
private func getItemsInfoFromCache() -> [MagicItem.Info] {
|
|
let fileURL = AppConstants.watchMagicItemsInfo
|
|
guard FileManager.default.fileExists(atPath: fileURL.path) else {
|
|
Current.Log.error("Watch magic items info cache file doesn't exist at path: \(fileURL.absoluteString)")
|
|
return []
|
|
}
|
|
|
|
let data = FileManager.default.contents(atPath: fileURL.path) ?? Data()
|
|
|
|
do {
|
|
let infos = try JSONDecoder().decode([MagicItem.Info].self, from: data)
|
|
return infos
|
|
} catch {
|
|
Current.Log.error("Failed to decode watch magic item info data from cache, error: \(error)")
|
|
return []
|
|
}
|
|
}
|
|
|
|
private func updateConfig(config: WatchConfig, magicItemsInfo: [MagicItem.Info]) {
|
|
DispatchQueue.main.async { [weak self] in
|
|
self?.watchConfig = config
|
|
self?.magicItemsInfo = magicItemsInfo
|
|
|
|
if config.assist.showAssist,
|
|
config.assist.serverId != nil,
|
|
config.assist.pipelineId != nil {
|
|
self?.showAssist = true
|
|
} else {
|
|
self?.showAssist = false
|
|
}
|
|
self?.refreshListID = UUID()
|
|
}
|
|
}
|
|
|
|
private func updateLoading(isLoading: Bool) {
|
|
DispatchQueue.main.async { [weak self] in
|
|
self?.isLoading = isLoading
|
|
}
|
|
}
|
|
|
|
private func displayError(message: String) {
|
|
DispatchQueue.main.async { [weak self] in
|
|
self?.errorMessage = message
|
|
self?.showError = true
|
|
}
|
|
}
|
|
|
|
private func resetError() {
|
|
DispatchQueue.main.async { [weak self] in
|
|
self?.errorMessage = ""
|
|
self?.showError = false
|
|
}
|
|
}
|
|
}
|