iOS/Sources/Extensions/Watch/Home/MagicItemRow/WatchMagicViewRowViewModel.swift
Bruno Pantaleão Gonçalves 266ceaaa75
Align network information access between iOS and Mac (#3939)
<!-- 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 -->
Align implementation to use Current.connectivity.currentSSID() a source
of truth for macOS and iOS

## 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. -->
2025-11-07 14:53:46 +01:00

172 lines
5.4 KiB
Swift

import Communicator
import Foundation
import NetworkExtension
import PromiseKit
import Shared
final class WatchMagicViewRowViewModel: ObservableObject {
enum RowState {
case idle
case loading
case success
case failure
}
enum MagicItemResponse {
case success
case failed
case tookLonger
var rowState: RowState {
switch self {
case .success:
return .success
case .failed:
return .failure
case .tookLonger:
return .idle
}
}
}
@Published private(set) var state: RowState = .idle
@Published var showConfirmationDialog = false
@Published private(set) var item: MagicItem
@Published private(set) var itemInfo: MagicItem.Info
private var timeoutWorkItem: DispatchWorkItem?
init(item: MagicItem, itemInfo: MagicItem.Info) {
self.item = item
self.itemInfo = itemInfo
}
func executeItem() {
if itemInfo.customization?.requiresConfirmation ?? true {
showConfirmationDialog = true
} else {
executeItemAction()
}
}
func confirmationAction() {
executeItemAction()
}
private func executeItemAction() {
state = .loading
executeMagicItem { [weak self] response in
DispatchQueue.main.async { [weak self] in
self?.state = response.rowState
}
self?.resetState()
}
}
private func resetState() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
self?.state = .idle
}
}
private func fetchNetworkInfo(completion: (() -> Void)? = nil) {
Current.networkInformation { hotspotNetwork in
WatchUserDefaults.shared.set(hotspotNetwork?.ssid, key: .watchSSID)
completion?()
}
}
private func executeMagicItemUsingiPhone(magicItem: MagicItem, completion: @escaping (Bool) -> Void) {
Current.Log.verbose("Signaling magic item pressed via phone")
let itemMessage = InteractiveImmediateMessage(
identifier: InteractiveImmediateMessages.magicItemPressed.rawValue,
content: [
"itemId": magicItem.id,
"serverId": magicItem.serverId,
"itemType": magicItem.type.rawValue,
],
reply: { message in
Current.Log.verbose("Received reply dictionary \(message)")
if message.content["fired"] as? Bool == true {
completion(true)
} else {
completion(false)
}
}
)
Current.Log
.verbose(
"Sending \(InteractiveImmediateMessages.magicItemPressed.rawValue) message \(itemMessage)"
)
Communicator.shared.send(itemMessage, errorHandler: { error in
Current.Log.error("Received error when sending immediate message \(error)")
completion(false)
})
}
private func executeMagicItemUsingAPI(magicItem: MagicItem, completion: @escaping (Bool) -> Void) {
guard let server = Current.servers.all.first(where: { $0.identifier.rawValue == magicItem.serverId }) else {
completion(false)
return
}
Current.Log.error("Executing watch magic item directly via API")
magicItem.execute(on: server, source: .Watch) { success in
completion(success)
}
}
private func executeMagicItem(completion: @escaping (MagicItemResponse) -> Void) {
let timeTriggered = Date()
let magicItem = item
Current.Log.verbose("Selected magic item id: \(magicItem.id)")
fetchNetworkInfo { [weak self] in
guard let self else { return }
if Communicator.shared.currentReachability == .immediatelyReachable {
executeMagicItemUsingiPhone(magicItem: magicItem) { success in
// Avoid haptics in background
guard self.isLessThan30Seconds(from: timeTriggered) else {
completion(.tookLonger)
return
}
if success {
self.cancelTimeout()
completion(success ? .success : .failed)
} else {
completion(.failed)
}
}
} else {
executeMagicItemUsingAPI(magicItem: magicItem) { success in
self.cancelTimeout()
completion(success ? .success : .failed)
}
}
startTimeoutTimerWhichResetsState(completion: completion)
}
}
// Given date returns if is less than 30 seconds from now
private func isLessThan30Seconds(from date: Date) -> Bool {
Date().timeIntervalSince(date) < 30
}
private func startTimeoutTimerWhichResetsState(completion: @escaping (MagicItemResponse) -> Void) {
timeoutWorkItem?.cancel()
timeoutWorkItem = DispatchWorkItem {
completion(.tookLonger)
}
if let workItem = timeoutWorkItem {
DispatchQueue.main.asyncAfter(deadline: .now() + 4, execute: workItem)
}
}
private func cancelTimeout() {
timeoutWorkItem?.cancel()
}
}