@@ -21,9 +21,13 @@ private var carPlayMaxItems: Int {
2121final class CarPlaySceneDelegate : UIResponder , CPTemplateApplicationSceneDelegate {
2222 private var interfaceController : CPInterfaceController ?
2323 private var streamTask : Task < Void , Never > ?
24+ private var refreshTask : Task < Void , Never > ?
2425 private var preferencesTask : Task < Void , Never > ?
2526 private let sitemapEventStream = SitemapEventStream ( )
2627 private var currentGridTemplate : CPGridTemplate ?
28+ // Retained across SSE restarts so page refreshes don't need a full stream teardown.
29+ private var currentPage : OpenHABPage ?
30+ private var currentService : OpenAPIService ?
2731
2832 func templateApplicationScene( _ templateApplicationScene: CPTemplateApplicationScene ,
2933 didConnect interfaceController: CPInterfaceController ) {
@@ -38,11 +42,15 @@ final class CarPlaySceneDelegate: UIResponder, CPTemplateApplicationSceneDelegat
3842 didDisconnectInterfaceController interfaceController: CPInterfaceController ) {
3943 streamTask? . cancel ( )
4044 streamTask = nil
45+ refreshTask? . cancel ( )
46+ refreshTask = nil
4147 preferencesTask? . cancel ( )
4248 preferencesTask = nil
4349 Task { await sitemapEventStream. stop ( ) }
4450 self . interfaceController = nil
4551 currentGridTemplate = nil
52+ currentPage = nil
53+ currentService = nil
4654 Logger . carPlay. info ( " CarPlay scene disconnected " )
4755 }
4856
@@ -63,6 +71,7 @@ final class CarPlaySceneDelegate: UIResponder, CPTemplateApplicationSceneDelegat
6371 let newSitemap = Preferences . shared. currentHomePreferences. sitemapForCarPlay
6472 guard newSitemap != lastSitemap else { continue }
6573 lastSitemap = newSitemap
74+ // Sitemap selection changed — full restart needed (different page/subscription).
6675 startStreaming ( )
6776 }
6877 }
@@ -90,17 +99,17 @@ final class CarPlaySceneDelegate: UIResponder, CPTemplateApplicationSceneDelegat
9099 }
91100 do {
92101 let service = try OpenAPIService ( connectionConfiguration: connection. configuration)
102+ currentService = service
93103
94- // Initial fetch to populate the template and learn the pageId
95104 guard let page = try await service. pollDataForPage (
96105 sitemapname: sitemapName, pageId: " " , longPolling: false
97106 ) else { return }
107+ currentPage = page
98108 updateTemplate ( page: page, service: service)
99109
100- // Prefer SSE; fall back to long-poll on older servers
101110 let serverProps = try ? await service. getRoot ( )
102111 if serverProps? . hasSseSupport ( ) == true {
103- await runSSE ( page : page , sitemapName: sitemapName, connection: connection, service : service )
112+ await runSSE ( sitemapName: sitemapName, connection: connection)
104113 } else {
105114 await runLongPoll ( sitemapName: sitemapName, pageId: page. pageId, service: service)
106115 }
@@ -110,40 +119,50 @@ final class CarPlaySceneDelegate: UIResponder, CPTemplateApplicationSceneDelegat
110119 }
111120
112121 @MainActor
113- private func runSSE( page : OpenHABPage , sitemapName: String , connection: ConnectionInfo , service : OpenAPIService ) async {
122+ private func runSSE( sitemapName: String , connection: ConnectionInfo ) async {
114123 await sitemapEventStream. startMonitoringNetworkIfNeeded ( initialConnection: connection)
115- let pageId = page . pageId. isEmpty ? sitemapName : page . pageId
124+ let pageId = currentPage . map { $0 . pageId. isEmpty ? sitemapName : $0 . pageId } ?? sitemapName
116125 let stream = await sitemapEventStream. stream ( sitemap: sitemapName, pageId: pageId)
117126 Logger . carPlay. info ( " CarPlay SSE starting for \( sitemapName) / \( pageId) " )
118127
128+ // Refresh on reconnect so structural changes missed during a disconnect are caught.
129+ var needsRefreshOnReconnect = false
130+
119131 for await msg in stream {
120132 guard !Task. isCancelled else { break }
121133 switch msg {
122134 case . connected:
123135 Logger . carPlay. info ( " CarPlay SSE connected " )
136+ if needsRefreshOnReconnect {
137+ needsRefreshOnReconnect = false
138+ schedulePageRefresh ( sitemapName: sitemapName)
139+ }
124140 case let . disconnected( error) :
141+ needsRefreshOnReconnect = true
125142 if let error { Logger . carPlay. warning ( " CarPlay SSE disconnected: \( error) " ) }
126143 case let . event( message) :
127- handleSseMessage ( message, page : page , service : service )
144+ handleSseMessage ( message, sitemapName : sitemapName )
128145 }
129146 }
130147 }
131148
132149 @MainActor
133- private func handleSseMessage( _ message: SitemapEventMessage , page : OpenHABPage , service : OpenAPIService ) {
150+ private func handleSseMessage( _ message: SitemapEventMessage , sitemapName : String ) {
134151 switch message {
135152 case . alive:
136153 break
137154 case . sitemapChanged:
138- Logger . carPlay. info ( " CarPlay SSE: sitemap changed, reloading " )
139- startStreaming ( )
155+ Logger . carPlay. info ( " CarPlay SSE: sitemap changed, refreshing page " )
156+ // Refresh page content only — do not restart the SSE stream.
157+ schedulePageRefresh ( sitemapName: sitemapName)
140158 case let . widget( event) :
159+ guard let page = currentPage, let service = currentService else { return }
141160 switch page. apply ( event: event) {
142161 case . applied:
143162 updateTemplate ( page: page, service: service)
144163 case . requiresPageReload, . notFound:
145- Logger . carPlay. info ( " CarPlay SSE: widget requires reload " )
146- startStreaming ( )
164+ Logger . carPlay. info ( " CarPlay SSE: widget \( event . widgetId ?? " " ) requires reload" )
165+ schedulePageRefresh ( sitemapName : sitemapName )
147166 case . unchanged:
148167 break
149168 }
@@ -152,13 +171,38 @@ final class CarPlaySceneDelegate: UIResponder, CPTemplateApplicationSceneDelegat
152171 }
153172 }
154173
174+ /// Refreshes `currentPage` from the server and updates the template without restarting the SSE stream.
175+ /// Cancels any in-flight refresh so rapid events coalesce into a single fetch.
176+ private func schedulePageRefresh( sitemapName: String ) {
177+ refreshTask? . cancel ( )
178+ refreshTask = Task { [ weak self] in
179+ guard let self, !Task. isCancelled else { return }
180+ await refreshPage ( sitemapName: sitemapName)
181+ }
182+ }
183+
184+ @MainActor
185+ private func refreshPage( sitemapName: String ) async {
186+ guard let service = currentService else { return }
187+ do {
188+ guard let page = try await service. pollDataForPage (
189+ sitemapname: sitemapName, pageId: " " , longPolling: false
190+ ) else { return }
191+ currentPage = page
192+ updateTemplate ( page: page, service: service)
193+ } catch {
194+ Logger . carPlay. error ( " CarPlay page refresh error: \( error) " )
195+ }
196+ }
197+
155198 @MainActor
156199 private func runLongPoll( sitemapName: String , pageId: String , service: OpenAPIService ) async {
157200 Logger . carPlay. info ( " CarPlay using long-poll for \( sitemapName) " )
158201 do {
159202 for try await event in SitemapPageLoader . stream ( sitemapName: sitemapName, pageId: pageId, service: service) {
160203 guard !Task. isCancelled else { break }
161204 if case let . longPoll( page, _) = event {
205+ currentPage = page
162206 updateTemplate ( page: page, service: service)
163207 }
164208 }
0 commit comments