@@ -19,6 +19,7 @@ import io.homeassistant.companion.android.common.data.integration.impl.entities.
19
19
import io.homeassistant.companion.android.common.data.integration.impl.entities.FireEventRequest
20
20
import io.homeassistant.companion.android.common.data.integration.impl.entities.GetConfigIntegrationRequest
21
21
import io.homeassistant.companion.android.common.data.integration.impl.entities.GetZonesIntegrationRequest
22
+ import io.homeassistant.companion.android.common.data.integration.impl.entities.IntegrationRequest
22
23
import io.homeassistant.companion.android.common.data.integration.impl.entities.RateLimitRequest
23
24
import io.homeassistant.companion.android.common.data.integration.impl.entities.RateLimitResponse
24
25
import io.homeassistant.companion.android.common.data.integration.impl.entities.RegisterDeviceIntegrationRequest
@@ -37,13 +38,17 @@ import io.homeassistant.companion.android.common.data.websocket.impl.entities.As
37
38
import io.homeassistant.companion.android.common.data.websocket.impl.entities.AssistPipelineEventType
38
39
import io.homeassistant.companion.android.common.data.websocket.impl.entities.AssistPipelineIntentEnd
39
40
import io.homeassistant.companion.android.common.data.websocket.impl.entities.GetConfigResponse
41
+ import io.homeassistant.companion.android.common.util.FailFast
42
+ import io.homeassistant.companion.android.database.server.Server
40
43
import java.util.concurrent.TimeUnit
41
44
import javax.inject.Named
42
45
import kotlinx.coroutines.flow.Flow
43
46
import kotlinx.coroutines.flow.filter
44
47
import kotlinx.coroutines.flow.flow
45
48
import kotlinx.coroutines.flow.map
46
49
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
50
+ import okhttp3.ResponseBody
51
+ import retrofit2.Response
47
52
import timber.log.Timber
48
53
49
54
class IntegrationRepositoryImpl @AssistedInject constructor(
@@ -127,44 +132,25 @@ class IntegrationRepositoryImpl @AssistedInject constructor(
127
132
128
133
override suspend fun updateRegistration (deviceRegistration : DeviceRegistration , allowReregistration : Boolean ) {
129
134
val request = RegisterDeviceIntegrationRequest (createUpdateRegistrationRequest(deviceRegistration))
130
- var causeException: Exception ? = null
131
- for (it in server.connection.getApiUrls()) {
132
- try {
133
- val response = integrationService.callWebhook(it.toHttpUrlOrNull()!! , request)
134
- // The server should return a body with the registration, but might return:
135
- // 200 with empty body for broken direct webhook
136
- // 404 for broken cloudhook
137
- // 410 for missing config entry
138
- if (response.isSuccessful) {
139
- if (response.code() == 200 && (response.body()?.contentLength() ? : 0 ) == 0L ) {
140
- throw IllegalStateException (" update_registration returned empty body" )
141
- } else {
142
- persistDeviceRegistration(deviceRegistration)
143
- return
135
+ server.callWebhookOnUrls(request, onSuccess = { response ->
136
+ // The server should return a body with the registration, but might return:
137
+ // 200 with empty body for broken direct webhook
138
+ if (response.code() == 200 && response.body()?.contentLength() == 0L ) {
139
+ Timber .w(" update_registration returned empty body" )
140
+ if (allowReregistration) {
141
+ Timber .w(" Device registration broken and reregistration allowed, reregistering" )
142
+ try {
143
+ registerDevice(deviceRegistration)
144
+ } catch (e: Exception ) {
145
+ throw IntegrationException (" Device registration broken and reregistration failed" , e)
144
146
}
145
- } else if (response.code() == 404 || response.code() == 410 ) {
146
- throw IllegalStateException (" update_registration returned code ${response.code()} " )
147
- }
148
- } catch (e: Exception ) {
149
- if (causeException == null ) causeException = e
150
- // Ignore failure until we are out of URLS to try, but use the first exception as cause exception
151
- }
152
- }
153
-
154
- if (causeException != null ) {
155
- if (allowReregistration && (causeException is IllegalStateException )) {
156
- Timber .w(causeException, " Device registration broken, reregistering" )
157
- try {
158
- registerDevice(deviceRegistration)
159
- } catch (e: Exception ) {
160
- throw IntegrationException (e)
147
+ } else {
148
+ throw IntegrationException (" Device registration broken and reregistration not allowed." )
161
149
}
162
150
} else {
163
- throw IntegrationException (causeException )
151
+ persistDeviceRegistration(deviceRegistration )
164
152
}
165
- } else {
166
- throw IntegrationException (" Error calling integration request update_registration" )
167
- }
153
+ })
168
154
}
169
155
170
156
override suspend fun getRegistration (): DeviceRegistration {
@@ -243,137 +229,38 @@ class IntegrationRepositoryImpl @AssistedInject constructor(
243
229
244
230
override suspend fun updateLocation (updateLocation : UpdateLocation ) {
245
231
val updateLocationRequest = createUpdateLocation(updateLocation)
246
-
247
- var causeException: Exception ? = null
248
- for (it in server.connection.getApiUrls()) {
249
- var wasSuccess = false
250
- try {
251
- wasSuccess =
252
- integrationService.callWebhook(
253
- it.toHttpUrlOrNull()!! ,
254
- updateLocationRequest,
255
- ).isSuccessful
256
- } catch (e: Exception ) {
257
- if (causeException == null ) causeException = e
258
- // Ignore failure until we are out of URLS to try, but use the first exception as cause exception
259
- }
260
- // if we had a successful call we can return
261
- if (wasSuccess) {
262
- return
263
- }
264
- }
265
-
266
- if (causeException != null ) {
267
- throw IntegrationException (causeException)
268
- } else {
269
- throw IntegrationException (" Error calling integration request update_location" )
270
- }
232
+ server.callWebhookOnUrls(updateLocationRequest)
271
233
}
272
234
273
235
override suspend fun callAction (
274
236
domain : String ,
275
237
action : String ,
276
238
actionData : Map <String , Any ?>,
277
239
) {
278
- var wasSuccess = false
279
-
280
- val actionRequest =
281
- ActionRequest (
282
- domain,
283
- action,
284
- actionData,
285
- )
286
-
287
- var causeException: Exception ? = null
288
- for (it in server.connection.getApiUrls()) {
289
- try {
290
- wasSuccess =
291
- integrationService.callWebhook(
292
- it.toHttpUrlOrNull()!! ,
293
- CallServiceIntegrationRequest (
294
- actionRequest,
295
- ),
296
- ).isSuccessful
297
- } catch (e: Exception ) {
298
- if (causeException == null ) causeException = e
299
- // Ignore failure until we are out of URLS to try, but use the first exception as cause exception
300
- }
301
- // if we had a successful call we can return
302
- if (wasSuccess) {
303
- return
304
- }
305
- }
306
-
307
- if (causeException != null ) {
308
- throw IntegrationException (causeException)
309
- } else {
310
- throw IntegrationException (" Error calling integration request call_service" )
311
- }
240
+ server.callWebhookOnUrls(
241
+ CallServiceIntegrationRequest (
242
+ ActionRequest (
243
+ domain,
244
+ action,
245
+ actionData,
246
+ ),
247
+ ),
248
+ )
312
249
}
313
250
314
251
override suspend fun scanTag (data : Map <String , String >) {
315
- var wasSuccess = false
316
-
317
- var causeException: Exception ? = null
318
- for (it in server.connection.getApiUrls()) {
319
- try {
320
- wasSuccess =
321
- integrationService.callWebhook(
322
- it.toHttpUrlOrNull()!! ,
323
- ScanTagIntegrationRequest (
324
- data,
325
- ),
326
- ).isSuccessful
327
- } catch (e: Exception ) {
328
- if (causeException == null ) causeException = e
329
- // Ignore failure until we are out of URLS to try, but use the first exception as cause exception
330
- }
331
- // if we had a successful call we can return
332
- if (wasSuccess) {
333
- return
334
- }
335
- }
336
-
337
- if (causeException != null ) {
338
- throw IntegrationException (causeException)
339
- } else {
340
- throw IntegrationException (" Error calling integration request scan_tag" )
341
- }
252
+ server.callWebhookOnUrls(ScanTagIntegrationRequest (data))
342
253
}
343
254
344
255
override suspend fun fireEvent (eventType : String , eventData : Map <String , Any >) {
345
- var wasSuccess = false
346
-
347
- val fireEventRequest = FireEventRequest (
348
- eventType,
349
- eventData.plus(Pair (" device_id" , deviceId)),
256
+ server.callWebhookOnUrls(
257
+ FireEventIntegrationRequest (
258
+ FireEventRequest (
259
+ eventType,
260
+ eventData.plus(Pair (" device_id" , deviceId)),
261
+ ),
262
+ ),
350
263
)
351
-
352
- var causeException: Exception ? = null
353
- for (it in server.connection.getApiUrls()) {
354
- try {
355
- wasSuccess =
356
- integrationService.callWebhook(
357
- it.toHttpUrlOrNull()!! ,
358
- FireEventIntegrationRequest (
359
- fireEventRequest,
360
- ),
361
- ).isSuccessful
362
- } catch (e: Exception ) {
363
- if (causeException == null ) causeException = e
364
- // Ignore failure until we are out of URLS to try, but use the first exception as cause exception
365
- }
366
- // if we had a successful call we can return
367
- if (wasSuccess) {
368
- return
369
- }
370
- }
371
-
372
- if (causeException != null ) {
373
- throw IntegrationException (causeException)
374
- } else {
375
- throw IntegrationException (" Error calling integration request fire_event" )
376
- }
377
264
}
378
265
379
266
override suspend fun getZones (): List <Entity > {
@@ -727,25 +614,9 @@ class IntegrationRepositoryImpl @AssistedInject constructor(
727
614
728
615
val integrationRequest = RegisterSensorIntegrationRequest (registrationData)
729
616
730
- var causeException: Exception ? = null
731
- for (it in server.connection.getApiUrls()) {
732
- try {
733
- integrationService.callWebhook(it.toHttpUrlOrNull()!! , integrationRequest)
734
- .let {
735
- // If we created sensor or it already exists
736
- if (it.isSuccessful || it.code() == 409 ) {
737
- return
738
- }
739
- }
740
- } catch (e: Exception ) {
741
- if (causeException == null ) causeException = e
742
- // Ignore failure until we are out of URLS to try, but use the first exception as cause exception
743
- }
744
- }
745
- if (causeException != null ) {
746
- throw IntegrationException (causeException)
747
- } else {
748
- throw IntegrationException (" Error calling integration request register_sensor" )
617
+ server.callWebhookOnUrls(integrationRequest) { response ->
618
+ // If we created sensor or it already exists
619
+ response.isSuccessful || response.code() == 409
749
620
}
750
621
}
751
622
@@ -797,6 +668,42 @@ class IntegrationRepositoryImpl @AssistedInject constructor(
797
668
}
798
669
}
799
670
671
+ private suspend fun Server.callWebhookOnUrls (
672
+ request : IntegrationRequest ,
673
+ onSuccess : suspend (response: Response <ResponseBody >) -> Unit = {},
674
+ isValidResponse : (response: Response <ResponseBody >) -> Boolean = { response -> response.isSuccessful },
675
+ ) {
676
+ var firstCauseException: Exception ? = null
677
+ val httpURLs = connection.getApiUrls().mapNotNull { it.toHttpUrlOrNull() }
678
+
679
+ if (httpURLs.isEmpty()) {
680
+ throw IntegrationException (" No valid url can be found in server connection ${if (BuildConfig .DEBUG ) connection.getApiUrls() else " " } " )
681
+ }
682
+
683
+ httpURLs.forEach { url ->
684
+ try {
685
+ val response = integrationService.callWebhook(url, request)
686
+ if (isValidResponse(response)) {
687
+ onSuccess(response)
688
+ // If the response is successful we return and ignore potential firstCauseException
689
+ return
690
+ } else {
691
+ throw IntegrationException (
692
+ " Error calling webhook" ,
693
+ response.code(),
694
+ response.errorBody(),
695
+ )
696
+ }
697
+ } catch (e: Exception ) {
698
+ if (firstCauseException == null ) firstCauseException = e
699
+ // Ignore failure until we are out of URLS to try, but use the first exception as cause exception
700
+ }
701
+ }
702
+
703
+ firstCauseException?.let { throw it }
704
+ FailFast .fail { " Error calling webhook without cause." }
705
+ }
706
+
800
707
private suspend fun createUpdateRegistrationRequest (deviceRegistration : DeviceRegistration ): RegisterDeviceRequest {
801
708
val oldDeviceRegistration = getRegistration()
802
709
val pushToken = deviceRegistration.pushToken ? : oldDeviceRegistration.pushToken
0 commit comments