mirror of
https://github.com/home-assistant/iOS.git
synced 2026-02-05 06:35:37 -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 --> This PR allows adding supported domain entities to CarPlay, Widgets and Apple watch directly from the entity more info dialog ## 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. --> --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
205 lines
6.7 KiB
Swift
205 lines
6.7 KiB
Swift
import Foundation
|
|
@preconcurrency import Shared
|
|
|
|
/// Represents an action that allows users to add a Home Assistant entity to various iOS
|
|
/// platform features (widgets, shortcuts, etc.) or connected devices (Watch, CarPlay).
|
|
///
|
|
/// This protocol is used for bidirectional communication between the iOS app and the
|
|
/// Home Assistant frontend. The frontend can query available actions, and the user can select
|
|
/// which action to perform for a specific entity.
|
|
protocol EntityAddToAction: Codable {
|
|
/// The Material Design Icon identifier for this action, used for visual representation in the UI.
|
|
///
|
|
/// The format must be `mdi:NAME_OF_ASSET`, for example `mdi:car`.
|
|
var mdiIcon: String { get }
|
|
|
|
/// Indicates whether this action is currently available for use. The frontend is going to display
|
|
/// this action but it won't be usable.
|
|
///
|
|
/// Some actions may be disabled based on system limitations or device state.
|
|
var enabled: Bool { get }
|
|
|
|
/// Returns the localized display text for this action.
|
|
///
|
|
/// This text is shown to the user in the action selection UI and should clearly describe
|
|
/// what the action will do.
|
|
func text() -> String
|
|
|
|
/// Returns optional additional details or status information about this action.
|
|
///
|
|
/// This can be used to provide context about why an action might be disabled or other
|
|
/// relevant information.
|
|
func details() -> String?
|
|
|
|
/// Returns the action type identifier for serialization
|
|
var actionType: String { get }
|
|
}
|
|
|
|
extension EntityAddToAction {
|
|
var enabled: Bool { true }
|
|
func details() -> String? { nil }
|
|
}
|
|
|
|
/// Concrete action types that can be performed
|
|
enum EntityAddToActionType: String, Codable {
|
|
case carPlayQuickAccess
|
|
case watchItem
|
|
case customWidget
|
|
}
|
|
|
|
// MARK: - Action Implementations
|
|
|
|
/// Action to add an entity to CarPlay quick access
|
|
struct CarPlayQuickAccessAction: EntityAddToAction {
|
|
var mdiIcon: String { "mdi:car" }
|
|
var actionType: String { EntityAddToActionType.carPlayQuickAccess.rawValue }
|
|
|
|
func text() -> String {
|
|
L10n.WebView.AddTo.Option.CarPlay.title
|
|
}
|
|
}
|
|
|
|
/// Action to add an entity to Watch favorites
|
|
struct WatchItemAction: EntityAddToAction {
|
|
var mdiIcon: String { "mdi:watch" }
|
|
var actionType: String { EntityAddToActionType.watchItem.rawValue }
|
|
|
|
func text() -> String {
|
|
L10n.WebView.AddTo.Option.AppleWatch.title
|
|
}
|
|
}
|
|
|
|
/// Action to add a custom widget
|
|
struct CustomWidgetAction: EntityAddToAction {
|
|
var mdiIcon: String { "mdi:shape" }
|
|
var actionType: String { EntityAddToActionType.customWidget.rawValue }
|
|
|
|
func text() -> String {
|
|
L10n.WebView.AddTo.Option.Widget.title
|
|
}
|
|
}
|
|
|
|
// MARK: - External Representation
|
|
|
|
/// External representation of an action for communication with the frontend
|
|
struct ExternalEntityAddToAction: Codable {
|
|
let appPayload: String
|
|
let enabled: Bool
|
|
let name: String
|
|
let details: String?
|
|
let mdiIcon: String
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case appPayload = "app_payload"
|
|
case enabled
|
|
case name
|
|
case details
|
|
case mdiIcon = "mdi_icon"
|
|
}
|
|
|
|
/// Converts the action to a dictionary suitable for the external bus response
|
|
func toDictionary() -> [String: Any] {
|
|
[
|
|
CodingKeys.appPayload.rawValue: appPayload,
|
|
CodingKeys.enabled.rawValue: enabled,
|
|
CodingKeys.name.rawValue: name,
|
|
CodingKeys.details.rawValue: details as Any,
|
|
CodingKeys.mdiIcon.rawValue: mdiIcon,
|
|
]
|
|
}
|
|
|
|
/// Creates an external representation from an action
|
|
static func from(action: any EntityAddToAction) throws -> ExternalEntityAddToAction {
|
|
// Encode the action to JSON
|
|
let encoder = JSONEncoder()
|
|
let actionData = try encoder.encode(AnyEntityAddToAction(action))
|
|
|
|
// Convert to base64 to ensure data integrity when round-tripping through the frontend
|
|
let appPayload = actionData.base64EncodedString()
|
|
|
|
return ExternalEntityAddToAction(
|
|
appPayload: appPayload,
|
|
enabled: action.enabled,
|
|
name: action.text(),
|
|
details: action.details(),
|
|
mdiIcon: action.mdiIcon
|
|
)
|
|
}
|
|
|
|
/// Decodes an action from the app payload
|
|
static func toAction(from appPayload: String) throws -> any EntityAddToAction {
|
|
guard let data = Data(base64Encoded: appPayload) else {
|
|
throw EntityAddToError.invalidPayload
|
|
}
|
|
|
|
let decoder = JSONDecoder()
|
|
let anyAction = try decoder.decode(AnyEntityAddToAction.self, from: data)
|
|
return anyAction.action
|
|
}
|
|
}
|
|
|
|
// MARK: - Type Erasure Helper
|
|
|
|
/// Type-erased wrapper for EntityAddToAction to enable encoding/decoding
|
|
private struct AnyEntityAddToAction: Codable {
|
|
let action: any EntityAddToAction
|
|
|
|
init(_ action: any EntityAddToAction) {
|
|
self.action = action
|
|
}
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case type
|
|
case data
|
|
}
|
|
|
|
init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
let type = try container.decode(EntityAddToActionType.self, forKey: .type)
|
|
|
|
switch type {
|
|
case .carPlayQuickAccess:
|
|
self.action = try container.decode(CarPlayQuickAccessAction.self, forKey: .data)
|
|
case .watchItem:
|
|
self.action = try container.decode(WatchItemAction.self, forKey: .data)
|
|
case .customWidget:
|
|
self.action = try container.decode(CustomWidgetAction.self, forKey: .data)
|
|
}
|
|
}
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
guard let type = EntityAddToActionType(rawValue: action.actionType) else {
|
|
throw EntityAddToError.encodingFailed
|
|
}
|
|
try container.encode(type, forKey: .type)
|
|
|
|
switch type {
|
|
case .carPlayQuickAccess:
|
|
if let typed = action as? CarPlayQuickAccessAction {
|
|
try container.encode(typed, forKey: .data)
|
|
} else {
|
|
throw EntityAddToError.encodingFailed
|
|
}
|
|
case .watchItem:
|
|
if let typed = action as? WatchItemAction {
|
|
try container.encode(typed, forKey: .data)
|
|
} else {
|
|
throw EntityAddToError.encodingFailed
|
|
}
|
|
case .customWidget:
|
|
if let typed = action as? CustomWidgetAction {
|
|
try container.encode(typed, forKey: .data)
|
|
} else {
|
|
throw EntityAddToError.encodingFailed
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
enum EntityAddToError: Error {
|
|
case invalidPayload
|
|
case encodingFailed
|
|
case decodingFailed
|
|
}
|