iOS/Sources/Shared/API/Models/AppEntitiesModel.swift
Copilot 3f25cd80ab
Add automation domain and iOS control widget (#4120)
## Summary

Adds support for Home Assistant automations in the iOS app. Automations
can now be triggered from iOS Control Center widgets (iOS 18+), similar
to existing script and scene controls.

**Domain & Model**
- Added `automation` domain with robot icon and `trigger` service
- Included automation in `AppEntitiesModel` tracked domains for database
caching
- Added to CarPlay supported domains

**Control Widget** (iOS 18+)
- `ControlAutomation` - Main control widget
- `AutomationAppIntent` - Triggers `automation.trigger` service
- `IntentAutomationEntity` - Entity queries and selection
- `ControlAutomationsValueProvider` - Configuration with custom
icons/text

**Pattern**
Follows existing `ControlScript` and `ControlScene` implementation
patterns for consistency.

**Tests**
- Updated `testWidgetsKindCasesValues` to include assertion for
`controlAutomation` case
- Updated widget kind count validation from 25 to 26

## Screenshots

N/A - Control widgets are system UI and cannot be screenshotted during
development. Widget appears in Control Center configuration with
automation icon and allows selection from available automations.

## Link to pull request in Documentation repository

Documentation: home-assistant/companion.home-assistant#

## Any other notes

Localization strings added for English only. Additional languages will
need translation via the standard localization workflow.

<!-- START COPILOT CODING AGENT SUFFIX -->



<!-- START COPILOT ORIGINAL PROMPT -->



<details>

<summary>Original prompt</summary>

> 1 - Add "automation" in AppEntitiesModel domains
> 2 - Create an iOS control for running automation like it exist for
ControlScript


</details>



<!-- START COPILOT CODING AGENT TIPS -->
---

 Let Copilot coding agent [set things up for
you](https://github.com/home-assistant/iOS/issues/new?title=+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot)
— coding agent works faster and does higher quality work when set up for
your repo.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: bgoncal <5808343+bgoncal@users.noreply.github.com>
2025-12-22 10:58:37 +00:00

117 lines
4.8 KiB
Swift

import Foundation
import GRDB
import HAKit
import PromiseKit
public protocol AppEntitiesModelProtocol {
func updateModel(_ entities: Set<HAEntity>, server: Server)
}
final class AppEntitiesModel: AppEntitiesModelProtocol {
static var shared = AppEntitiesModel()
/// ServerId: Date
private var lastDatabaseUpdate: [String: Date] = [:]
/// ServerId: Int
private var lastEntitiesCount: [String: Int] = [:]
private let domainsAppUse: [String] = [
Domain.automation,
Domain.scene,
Domain.script,
Domain.light,
Domain.switch,
Domain.sensor,
Domain.binarySensor,
Domain.cover,
Domain.button,
Domain.inputBoolean,
Domain.inputButton,
Domain.lock,
Domain.camera,
Domain.fan,
].map(\.rawValue)
public func updateModel(_ entities: Set<HAEntity>, server: Server) {
// Only update database after a few seconds or if the entities count changed
// First check for time to avoid unecessary filtering to check count
if !checkLastDatabaseUpdateRecently(server: server) {
let appRelatedEntities = filterDomains(entities)
Current.Log
.verbose(
"Updating App Entities for \(server.info.name) checkLastDatabaseUpdateLessThanMinuteAgo false, lastDatabaseUpdate \(String(describing: lastDatabaseUpdate)) "
)
updateLastUpdate(entitiesCount: appRelatedEntities.count, server: server)
handle(appRelatedEntities: appRelatedEntities, server: server)
} else {
let appRelatedEntities = filterDomains(entities)
if lastEntitiesCount[server.identifier.rawValue] != appRelatedEntities.count {
Current.Log
.verbose(
"Updating App Entities for \(server.info.name) entities count diff, count: last \(lastEntitiesCount), new \(appRelatedEntities.count)"
)
updateLastUpdate(entitiesCount: appRelatedEntities.count, server: server)
handle(appRelatedEntities: appRelatedEntities, server: server)
}
}
}
private func updateLastUpdate(entitiesCount: Int, server: Server) {
lastEntitiesCount[server.identifier.rawValue] = entitiesCount
lastDatabaseUpdate[server.identifier.rawValue] = Date()
}
private func filterDomains(_ entities: Set<HAEntity>) -> Set<HAEntity> {
entities.filter { domainsAppUse.contains($0.domain) }
}
// Avoid updating database too often
private func checkLastDatabaseUpdateRecently(server: Server) -> Bool {
guard let lastDate = lastDatabaseUpdate[server.identifier.rawValue] else { return false }
return Date().timeIntervalSince(lastDate) < 15
}
private func handle(appRelatedEntities: Set<HAEntity>, server: Server) {
let appEntities = appRelatedEntities.map({ HAAppEntity(
id: ServerEntity.uniqueId(serverId: server.identifier.rawValue, entityId: $0.entityId),
entityId: $0.entityId,
serverId: server.identifier.rawValue,
domain: $0.domain,
name: $0.attributes.friendlyName ?? $0.entityId,
icon: $0.attributes.icon,
rawDeviceClass: $0.attributes.dictionary["device_class"] as? String
) }).sorted(by: { $0.id < $1.id })
do {
let cachedEntities = try Current.database().read { db in
try HAAppEntity
.filter(Column(DatabaseTables.AppEntity.serverId.rawValue) == server.identifier.rawValue)
.orderByPrimaryKey()
.fetchAll(db)
}
if appEntities != cachedEntities {
Current.Log
.verbose(
"Updating App Entities for \(server.info.name), cached entities were different than new entities"
)
try Current.database().write { db in
try HAAppEntity.deleteAll(db, ids: cachedEntities.map(\.id))
for entity in appEntities {
try entity.insert(db)
}
}
Current.clientEventStore.addEvent(ClientEvent(
text: "Updated database App Entities for \(server.info.name)",
type: .database,
payload: ["entities_count": appEntities.count]
))
}
} catch {
Current.Log.error("Failed to get cache for App Entities, error: \(error.localizedDescription)")
Current.clientEventStore.addEvent(ClientEvent(
text: "Update database App Entities FAILED for \(server.info.name)",
type: .database,
payload: ["error": error.localizedDescription]
))
}
}
}