Use database areas instead of always fetching from API (#3967)

<!-- 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. -->

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Bruno Pantaleão Gonçalves
2025-11-12 21:07:17 +01:00
committed by GitHub
parent cee3842d85
commit 210c61ee63
5 changed files with 69 additions and 55 deletions

View File

@@ -9,6 +9,8 @@ final class WebViewSceneDelegate: NSObject, UIWindowSceneDelegate {
var windowController: WebViewWindowController?
var urlHandler: IncomingURLHandler?
private var updateDatabaseTask: Task<Void, Never>?
// swiftlint:disable cyclomatic_complexity
func scene(
_ scene: UIScene,
@@ -120,7 +122,10 @@ final class WebViewSceneDelegate: NSObject, UIWindowSceneDelegate {
}
func sceneDidBecomeActive(_ scene: UIScene) {
updateDatabase()
updateDatabaseTask?.cancel()
updateDatabaseTask = Task {
await updateDatabase()
}
cleanWidgetsCache()
updateLocation()
}
@@ -168,13 +173,13 @@ final class WebViewSceneDelegate: NSObject, UIWindowSceneDelegate {
}
/// Sets up model manager and update database tables for cached panels and entities
private func updateDatabase() {
private func updateDatabase() async {
Current.modelManager.cleanup().cauterize()
Current.modelManager.subscribe(isAppInForeground: {
UIApplication.shared.applicationState == .active
})
Current.appDatabaseUpdater.update()
await Current.appDatabaseUpdater.update()
Current.panelsUpdater.update()
}

View File

@@ -21,9 +21,6 @@ final class MagicItemAddViewModel: ObservableObject {
@Published var searchText: String = ""
@Published var selectedServerId: String?
/// [ServerId: [AreaId: Set<EntityId>]]
@Published var serversAreasAndItsEntities: [String: [String: Set<String>]] = [:]
private var entitiesSubscription: AnyCancellable?
init() {
@@ -55,19 +52,17 @@ final class MagicItemAddViewModel: ObservableObject {
func loadContent() {
loadAppEntities()
loadActions()
Task {
await loadEntitiesForAreas()
}
}
func subtitleForEntity(entity: HAAppEntity, serverId: String) -> String {
guard let areasAndItsEntities = serversAreasAndItsEntities[serverId] else {
return ""
}
for (areaId, entityIds) in areasAndItsEntities where entityIds.contains(entity.entityId) {
if let area = Current.areasProvider().area(for: areaId, serverId: serverId) {
// Fetch area from database based on entity
do {
let areas = try AppArea.fetchAreas(containingEntity: entity.entityId, serverId: serverId)
if let area = areas.first {
return area.name
}
} catch {
Current.Log.error("Failed to fetch area for entity from database: \(error.localizedDescription)")
}
return entity.entityId
}
@@ -89,14 +84,4 @@ final class MagicItemAddViewModel: ObservableObject {
.filter({ $0.Scene == nil })
.sorted(by: { $0.Position < $1.Position })
}
private func loadEntitiesForAreas() async {
for server in Current.servers.all {
let areasAndItsEntities = await Current.areasProvider().fetchAreasAndItsEntities(for: server)
let serverId = server.identifier.rawValue
await MainActor.run { [serverId, areasAndItsEntities] in
self.serversAreasAndItsEntities[serverId] = areasAndItsEntities
}
}
}
}

View File

@@ -21,6 +21,20 @@ class SettingsViewController: HAFormViewController {
}
let contentSections: ContentSection
private var updateDatabaseTask: Task<Void, Never>?
deinit {
updateDatabaseTask?.cancel()
updateDatabaseTask = nil
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
updateDatabaseTask?.cancel()
updateDatabaseTask = nil
}
init(contentSections: ContentSection = .all) {
self.contentSections = contentSections
super.init()
@@ -175,7 +189,10 @@ class SettingsViewController: HAFormViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
Current.appDatabaseUpdater.update()
updateDatabaseTask?.cancel()
updateDatabaseTask = Task {
await Current.appDatabaseUpdater.update()
}
}
@objc func openAbout(_ sender: UIButton) {

View File

@@ -32,9 +32,17 @@ final class CarPlayAreasViewModel {
currentTask?.cancel()
currentTask = Task {
let areasAndEntities = await Current.areasProvider().fetchAreasAndItsEntities(for: server)
// Fetch areas from database instead of always fetching from API
let areas: [AppArea]
do {
areas = try AppArea.fetchAreas(for: server.identifier.rawValue)
} catch {
Current.Log.error("Failed to fetch areas from database: \(error.localizedDescription)")
areas = []
}
await MainActor.run {
self.updateAreas(allEntitiesPerArea: areasAndEntities, server: server)
self.updateAreas(areas: areas, server: server)
}
}
}
@@ -44,34 +52,36 @@ final class CarPlayAreasViewModel {
entitiesListTemplate?.entitiesStateChange(serverId: serverId, entities: entities)
}
private func updateAreas(allEntitiesPerArea: [String: Set<String>], server: Server) {
let areas = Current.areasProvider().areas[server.identifier.rawValue]
let items = areas?.sorted(by: { a1, a2 in
@MainActor
private func updateAreas(areas: [AppArea], server: Server) {
let items = areas.sorted(by: { a1, a2 in
a1.name < a2.name
}).compactMap { area -> CPListItem? in
guard let entityIdsForAreaId = allEntitiesPerArea[area.areaId] else { return nil }
// Skip areas with no entities
guard !area.entities.isEmpty else { return nil }
let icon = MaterialDesignIcons(
serversideValueNamed: area.icon ?? "mdi:circle"
).carPlayIcon()
let item = CPListItem(text: area.name, detailText: nil, image: icon)
item.accessoryType = .disclosureIndicator
item.handler = { [weak self] _, completion in
self?.listItemHandler(area: area, entityIdsForAreaId: Array(entityIdsForAreaId), server: server)
self?.listItemHandler(area: area, server: server)
completion()
}
return item
} ?? []
}
templateProvider?.paginatedList.updateItems(items: items)
}
// swiftlint:enable cyclomatic_complexity
private func listItemHandler(area: HAAreaResponse, entityIdsForAreaId: [String], server: Server) {
private func listItemHandler(area: AppArea, server: Server) {
guard let entities else { return }
entitiesListTemplate = CarPlayEntitiesListTemplate.build(
title: area.name,
filterType: .areaId(entityIds: entityIdsForAreaId),
filterType: .areaId(entityIds: Array(area.entities)),
server: server,
entitiesCachedStates: entities
)

View File

@@ -5,7 +5,7 @@ import UIKit
public protocol AppDatabaseUpdaterProtocol {
func stop()
func update()
func update() async
}
final class AppDatabaseUpdater: AppDatabaseUpdaterProtocol {
@@ -27,7 +27,7 @@ final class AppDatabaseUpdater: AppDatabaseUpdaterProtocol {
cancelOnGoingRequests()
}
func update() {
func update() async {
cancelOnGoingRequests()
if let lastUpdate, lastUpdate.timeIntervalSinceNow > -5 {
@@ -38,8 +38,9 @@ final class AppDatabaseUpdater: AppDatabaseUpdaterProtocol {
}
Current.Log.verbose("Updating database, servers count \(Current.servers.all.count)")
Current.servers.all.forEach { server in
guard server.info.connection.activeURL() != nil else { return }
for server in Current.servers.all {
guard server.info.connection.activeURL() != nil else { continue }
// Cache entities
let entitiesDatabaseToken = updateEntitiesDatabase(server: server)
requestTokens.append(entitiesDatabaseToken)
@@ -49,7 +50,7 @@ final class AppDatabaseUpdater: AppDatabaseUpdaterProtocol {
requestTokens.append(entitiesRegistryToken)
// Cache areas with their entities
updateAreasDatabase(server: server)
await updateAreasDatabase(server: server)
}
}
@@ -95,23 +96,19 @@ final class AppDatabaseUpdater: AppDatabaseUpdaterProtocol {
)
}
private func updateAreasDatabase(server: Server) {
Task { [weak self] in
guard let self else { return }
private func updateAreasDatabase(server: Server) async {
let areasAndEntities = await Current.areasProvider().fetchAreasAndItsEntities(for: server)
let areasAndEntities = await Current.areasProvider().fetchAreasAndItsEntities(for: server)
guard let areas = Current.areasProvider().areas[server.identifier.rawValue] else {
Current.Log.verbose("No areas found for server \(server.info.name)")
return
}
await saveAreasToDatabase(
areas: areas,
areasAndEntities: areasAndEntities,
serverId: server.identifier.rawValue
)
guard let areas = Current.areasProvider().areas[server.identifier.rawValue] else {
Current.Log.verbose("No areas found for server \(server.info.name)")
return
}
await saveAreasToDatabase(
areas: areas,
areasAndEntities: areasAndEntities,
serverId: server.identifier.rawValue
)
}
private func saveAreasToDatabase(