Skip to content

Commit 38ef80d

Browse files
authored
CarPlay sensor states fix + remove domains as default tab (#4467)
<!-- 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. -->
1 parent 3613bb8 commit 38ef80d

File tree

3 files changed

+83
-12
lines changed

3 files changed

+83
-12
lines changed

Sources/App/Scenes/CarPlaySceneDelegate.swift

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class CarPlaySceneDelegate: UIResponder {
4949
private var cachedConfig: CarPlayConfig?
5050
private var configObservation: AnyDatabaseCancellable?
5151
private var latestStates: HACachedStates?
52+
private var latestStatesServerId: String?
5253
private var latestQuickAccessStatesPerServer: [String: HACachedStates] = [:]
5354

5455
private var preferredServerId: String {
@@ -71,6 +72,23 @@ class CarPlaySceneDelegate: UIResponder {
7172
guard config != cachedConfig else { return }
7273
cachedConfig = config
7374
subscribeToQuickAccessEntitiesChanges(configEntities: cachedConfig?.quickAccessItems ?? [])
75+
76+
// Tabs can be removed from the configuration while their template instances are still
77+
// cached on the scene delegate. Clear those references before rebuilding so hidden
78+
// tabs stop receiving replays and state updates through `allTemplates`.
79+
if !config.tabs.contains(.quickAccess) {
80+
quickAccessListTemplate = nil
81+
}
82+
if !config.tabs.contains(.areas) {
83+
areasZonesListTemplate = nil
84+
}
85+
if !config.tabs.contains(.domains) {
86+
domainsListTemplate = nil
87+
}
88+
if !config.tabs.contains(.settings) {
89+
serversListTemplate = nil
90+
}
91+
7492
visibleTemplates = config.tabs.compactMap {
7593
switch $0 {
7694
case .quickAccess:
@@ -99,10 +117,15 @@ class CarPlaySceneDelegate: UIResponder {
99117
setInterfaceControllerForChildren()
100118
interfaceController?.setRootTemplate(tabBar, animated: true, completion: nil)
101119
updateTemplates()
120+
// The selected-server subscription may already have usable data when tabs are rebuilt.
121+
// Replay it so controls/areas do not flash empty while waiting for the next cache event.
122+
replaySelectedServerStates()
102123
}
103124

104125
private func buildQuickAccessTab() {
105126
quickAccessListTemplate = CarPlayQuickAccessTemplate.build()
127+
// Quick access keeps a separate per-server cache for its mixed-server entities,
128+
// so restore that snapshot immediately when the template is recreated.
106129
replayQuickAccessStates()
107130
}
108131

@@ -126,6 +149,8 @@ class CarPlaySceneDelegate: UIResponder {
126149
private func subscribeToEntitiesChanges() {
127150
guard let server = Current.servers.server(forServerIdentifier: preferredServerId) ?? Current.servers.all.first else { return }
128151
entitiesSubscriptionToken?.cancel()
152+
latestStates = nil
153+
latestStatesServerId = nil
129154

130155
var filter: [String: Any] = [:]
131156
if server.info.version > .canSubscribeEntitiesChangesWithFilter {
@@ -140,8 +165,9 @@ class CarPlaySceneDelegate: UIResponder {
140165
Current.api(for: server)?.connection.disconnect()
141166
entitiesSubscriptionToken = Current.api(for: server)?.connection.caches.states(filter)
142167
.subscribe { [weak self] _, states in
168+
self?.latestStates = states
169+
self?.latestStatesServerId = server.identifier.rawValue
143170
self?.allTemplates.forEach {
144-
self?.latestStates = states
145171
$0.entitiesStateChange(serverId: server.identifier.rawValue, entities: states)
146172
}
147173
}
@@ -189,6 +215,18 @@ class CarPlaySceneDelegate: UIResponder {
189215
}
190216
}
191217

218+
private func replaySelectedServerStates() {
219+
guard let latestStates, let latestStatesServerId else { return }
220+
221+
[
222+
areasZonesListTemplate,
223+
domainsListTemplate,
224+
serversListTemplate,
225+
].compactMap({ $0 }).forEach {
226+
$0.entitiesStateChange(serverId: latestStatesServerId, entities: latestStates)
227+
}
228+
}
229+
192230
private func observeCarPlayConfigChanges() {
193231
configObservation?.cancel()
194232
let observation = ValueObservation.tracking(CarPlayConfig.fetchOne)
@@ -232,6 +270,11 @@ extension CarPlaySceneDelegate: CPInterfaceControllerDelegate {
232270
if quickAccessListTemplate?.template == aTemplate {
233271
replayQuickAccessStates()
234272
}
273+
if domainsListTemplate?.template == aTemplate || areasZonesListTemplate?.template == aTemplate {
274+
// Navigating back to controls/areas does not guarantee a fresh websocket emission.
275+
// Reusing the latest selected-server snapshot keeps those tabs populated.
276+
replaySelectedServerStates()
277+
}
235278
allTemplates.forEach { $0.templateWillAppear(template: aTemplate) }
236279
}
237280
}

Sources/App/Settings/CarPlay/CarPlayConfig.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ import GRDB
44
public struct CarPlayConfig: Codable, FetchableRecord, PersistableRecord, Equatable {
55
public static var carPlayConfigId = "carplay-config"
66
public var id = CarPlayConfig.carPlayConfigId
7-
public var tabs: [CarPlayTab] = [.quickAccess, .areas, .domains, .settings]
7+
public var tabs: [CarPlayTab] = [.quickAccess, .areas, .settings]
88
public var quickAccessItems: [MagicItem] = []
99
public var quickAccessLayout: CarPlayQuickAccessLayout?
1010

1111
public init(
1212
id: String = CarPlayConfig.carPlayConfigId,
13-
tabs: [CarPlayTab] = [.quickAccess, .areas, .domains, .settings],
13+
tabs: [CarPlayTab] = [.quickAccess, .areas, .settings],
1414
quickAccessItems: [MagicItem] = [],
1515
quickAccessLayout: CarPlayQuickAccessLayout? = nil
1616
) {

Sources/CarPlay/Templates/QuickAccess/CarPlayQuickAccessTemplate.swift

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ final class CarPlayQuickAccessTemplate: CarPlayTemplateProvider {
3434
private var currentItems: [MagicItem] = []
3535
private var currentLayout: CarPlayQuickAccessLayout = .grid
3636
private var entitiesPerServer: [String: HACachedStates] = [:]
37+
private var lastKnownEntities: [String: HAEntity] = [:]
3738
private var executingItemIds: Set<String> = []
3839
private var executingStartedAt: [String: Date] = [:]
3940
private var pendingExecutingClearWorkItems: [String: DispatchWorkItem] = [:]
@@ -140,6 +141,7 @@ final class CarPlayQuickAccessTemplate: CarPlayTemplateProvider {
140141
func updateList(for items: [MagicItem], layout: CarPlayQuickAccessLayout) {
141142
currentItems = items
142143
currentLayout = layout
144+
pruneLastKnownEntities(for: items)
143145
guard !items.isEmpty else {
144146
presentIntroductionItem()
145147
return
@@ -169,17 +171,16 @@ final class CarPlayQuickAccessTemplate: CarPlayTemplateProvider {
169171
let info = info(for: magicItem)
170172
switch magicItem.type {
171173
case .entity:
172-
guard let placeholderItem = entitiesPerServer[magicItem.serverId]?.all
173-
.first(where: { $0.entityId == magicItem.id }) ?? placeholderEntity(id: magicItem.id),
174-
let rowDisplayItem = rowDisplayItem(for: magicItem, entityToAreaMap: entityToAreaMap) else {
174+
guard let entity = resolvedEntity(for: magicItem),
175+
let rowDisplayItem = rowDisplayItem(for: magicItem, entityToAreaMap: entityToAreaMap) else {
175176
return .init(text: "", detailText: "")
176177
}
177178
let entityProvider = CarPlayEntityListItem(
178179
serverId: magicItem.serverId,
179-
entity: placeholderItem,
180+
entity: entity,
180181
magicItem: magicItem,
181182
magicItemInfo: info,
182-
area: entityToAreaMap[placeholderItem.entityId]
183+
area: entityToAreaMap[entity.entityId]
183184
)
184185
let listItem = entityProvider.template
185186
if isExecuting(magicItem) {
@@ -404,6 +405,34 @@ final class CarPlayQuickAccessTemplate: CarPlayTemplateProvider {
404405
isExecuting(magicItem) ? CarPlayEntityListItem.executingSubtitle : defaultSubtitle
405406
}
406407

408+
private func entityCacheKey(serverId: String, entityId: String) -> String {
409+
"\(serverId)::\(entityId)"
410+
}
411+
412+
private func pruneLastKnownEntities(for items: [MagicItem]) {
413+
let validKeys = Set(
414+
items
415+
.filter { $0.type == .entity }
416+
.map { entityCacheKey(serverId: $0.serverId, entityId: $0.id) }
417+
)
418+
lastKnownEntities = lastKnownEntities.filter { validKeys.contains($0.key) }
419+
}
420+
421+
private func resolvedEntity(for magicItem: MagicItem) -> HAEntity? {
422+
let cacheKey = entityCacheKey(serverId: magicItem.serverId, entityId: magicItem.id)
423+
424+
if let entity = entitiesPerServer[magicItem.serverId]?[magicItem.id] {
425+
lastKnownEntities[cacheKey] = entity
426+
return entity
427+
}
428+
429+
if let entity = lastKnownEntities[cacheKey] {
430+
return entity
431+
}
432+
433+
return placeholderEntity(id: magicItem.id)
434+
}
435+
407436
private func rowDisplayItem(
408437
for magicItem: MagicItem,
409438
entityToAreaMap: [String: String]
@@ -412,16 +441,15 @@ final class CarPlayQuickAccessTemplate: CarPlayTemplateProvider {
412441

413442
switch magicItem.type {
414443
case .entity:
415-
guard let placeholderItem = entitiesPerServer[magicItem.serverId]?.all
416-
.first(where: { $0.entityId == magicItem.id }) ?? placeholderEntity(id: magicItem.id) else {
444+
guard let entity = resolvedEntity(for: magicItem) else {
417445
Current.Log.error("Failed to create placeholder entity for magic item id: \(magicItem.id)")
418446
return nil
419447
}
420448

421-
let area = entityToAreaMap[placeholderItem.entityId]
449+
let area = entityToAreaMap[entity.entityId]
422450
let entityProvider = CarPlayEntityListItem(
423451
serverId: magicItem.serverId,
424-
entity: placeholderItem,
452+
entity: entity,
425453
magicItem: magicItem,
426454
magicItemInfo: info,
427455
area: area

0 commit comments

Comments
 (0)