mirror of
https://github.com/home-assistant/android
synced 2024-09-06 17:23:32 +00:00
Support multiple subscriptions of same type in websocket and use in template widget (#2801)
* Update WebSocketRepository for multiple subscriptions of same type - Change the structure of how the websocket repository tracks individual subscriptions to allow subscribing multiple times with the same type, but different data * Update TemplateWidget to use template subscriptions - Instead of depending on entity state changes and refreshing any time there is a change, use the render_template subscription for the template widget to limit the amount of data and power used. To make this possible without too much abstractions, the TemplateWidget no longer implements BaseWidgetProvider. - Fix hardcoded "Loading" string * Handle potential null subscription ID - Implements #2795 for changes to tracked subscription ID * Handle failed subscribing similar to other failures (null) - When subscribing fails, return null and don't store an active subscription instead of continuing and returning a flow (which would never emit messages) * Update manifest
This commit is contained in:
parent
ebc6b76dda
commit
a995d9410b
|
@ -161,7 +161,7 @@
|
|||
<action android:name="android.bluetooth.device.action.ACL_DISCONNECTED" />
|
||||
<action android:name="io.homeassistant.companion.android.background.REQUEST_SENSORS_UPDATE" />
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
<action android:name="io.homeassistant.companion.android.widgets.template.BaseWidgetProvider.RECEIVE_DATA" />
|
||||
<action android:name="io.homeassistant.companion.android.widgets.template.TemplateWidget.RECEIVE_DATA" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
|
|
|
@ -2,9 +2,11 @@ package io.homeassistant.companion.android.widgets.template
|
|||
|
||||
import android.app.PendingIntent
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.appwidget.AppWidgetProvider
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.text.Html.fromHtml
|
||||
|
@ -17,52 +19,184 @@ import androidx.core.graphics.toColorInt
|
|||
import com.google.android.material.color.DynamicColors
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.homeassistant.companion.android.R
|
||||
import io.homeassistant.companion.android.common.data.integration.Entity
|
||||
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
|
||||
import io.homeassistant.companion.android.database.widget.TemplateWidgetDao
|
||||
import io.homeassistant.companion.android.database.widget.TemplateWidgetEntity
|
||||
import io.homeassistant.companion.android.database.widget.WidgetBackgroundType
|
||||
import io.homeassistant.companion.android.util.getAttribute
|
||||
import io.homeassistant.companion.android.widgets.BaseWidgetProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import io.homeassistant.companion.android.common.R as commonR
|
||||
|
||||
@AndroidEntryPoint
|
||||
class TemplateWidget : BaseWidgetProvider() {
|
||||
class TemplateWidget : AppWidgetProvider() {
|
||||
companion object {
|
||||
private const val TAG = "TemplateWidget"
|
||||
|
||||
const val UPDATE_VIEW =
|
||||
"io.homeassistant.companion.android.widgets.template.TemplateWidget.UPDATE_VIEW"
|
||||
const val RECEIVE_DATA =
|
||||
"io.homeassistant.companion.android.widgets.template.TemplateWidget.RECEIVE_DATA"
|
||||
|
||||
internal const val EXTRA_TEMPLATE = "extra_template"
|
||||
internal const val EXTRA_TEXT_SIZE = "EXTRA_TEXT_SIZE"
|
||||
internal const val EXTRA_BACKGROUND_TYPE = "EXTRA_BACKGROUND_TYPE"
|
||||
internal const val EXTRA_TEXT_COLOR = "EXTRA_TEXT_COLOR"
|
||||
|
||||
private var isSubscribed = false
|
||||
private var widgetScope: CoroutineScope? = null
|
||||
private val widgetTemplates = mutableMapOf<Int, String>()
|
||||
private val widgetJobs = mutableMapOf<Int, Job>()
|
||||
}
|
||||
|
||||
@Inject
|
||||
lateinit var integrationUseCase: IntegrationRepository
|
||||
|
||||
@Inject
|
||||
lateinit var templateWidgetDao: TemplateWidgetDao
|
||||
|
||||
private var thisSetScope = false
|
||||
private var lastIntent = ""
|
||||
|
||||
init {
|
||||
setupWidgetScope()
|
||||
}
|
||||
|
||||
private fun setupWidgetScope() {
|
||||
if (widgetScope == null || !widgetScope!!.isActive) {
|
||||
widgetScope = CoroutineScope(Dispatchers.Main + Job())
|
||||
thisSetScope = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUpdate(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetIds: IntArray
|
||||
) {
|
||||
// There may be multiple widgets active, so update all of them
|
||||
for (appWidgetId in appWidgetIds) {
|
||||
widgetScope?.launch {
|
||||
val views = getWidgetRemoteViews(context, appWidgetId)
|
||||
appWidgetManager.updateAppWidget(appWidgetId, views)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
|
||||
// When the user deletes the widget, delete the preference associated with it.
|
||||
mainScope.launch {
|
||||
widgetScope?.launch {
|
||||
templateWidgetDao.deleteAll(appWidgetIds)
|
||||
appWidgetIds.forEach {
|
||||
widgetTemplates.remove(it)
|
||||
widgetJobs[it]?.cancel()
|
||||
widgetJobs.remove(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun isSubscribed(): Boolean = isSubscribed
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
lastIntent = intent.action.toString()
|
||||
val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
|
||||
|
||||
override fun setSubscribed(subscribed: Boolean) {
|
||||
isSubscribed = subscribed
|
||||
super.onReceive(context, intent)
|
||||
when (lastIntent) {
|
||||
UPDATE_VIEW -> updateView(context, appWidgetId)
|
||||
RECEIVE_DATA -> {
|
||||
saveEntityConfiguration(
|
||||
context,
|
||||
intent.extras,
|
||||
appWidgetId
|
||||
)
|
||||
onScreenOn(context)
|
||||
}
|
||||
Intent.ACTION_SCREEN_ON -> onScreenOn(context)
|
||||
Intent.ACTION_SCREEN_OFF -> onScreenOff()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getWidgetProvider(context: Context): ComponentName =
|
||||
ComponentName(context, TemplateWidget::class.java)
|
||||
private fun onScreenOn(context: Context) {
|
||||
setupWidgetScope()
|
||||
widgetScope!!.launch {
|
||||
if (!integrationUseCase.isRegistered()) {
|
||||
return@launch
|
||||
}
|
||||
updateAllWidgets(context)
|
||||
|
||||
override suspend fun getAllWidgetIds(context: Context): List<Int> {
|
||||
return templateWidgetDao.getAll().map { it.id }
|
||||
val allWidgets = templateWidgetDao.getAll()
|
||||
val widgetsWithDifferentTemplate = allWidgets.filter { it.template != widgetTemplates[it.id] }
|
||||
if (widgetsWithDifferentTemplate.isNotEmpty()) {
|
||||
if (thisSetScope) {
|
||||
context.applicationContext.registerReceiver(
|
||||
this@TemplateWidget,
|
||||
IntentFilter(Intent.ACTION_SCREEN_OFF)
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getWidgetRemoteViews(context: Context, appWidgetId: Int, suggestedEntity: Entity<Map<String, Any>>?): RemoteViews {
|
||||
widgetsWithDifferentTemplate.forEach { widget ->
|
||||
widgetJobs[widget.id]?.cancel()
|
||||
|
||||
val templateUpdates = integrationUseCase.getTemplateUpdates(widget.template)
|
||||
if (templateUpdates != null) {
|
||||
widgetTemplates[widget.id] = widget.template
|
||||
widgetJobs[widget.id] = widgetScope!!.launch {
|
||||
templateUpdates.collect {
|
||||
onTemplateChanged(context, widget.id, it)
|
||||
}
|
||||
}
|
||||
} else { // Remove data to make it retry on the next update
|
||||
widgetTemplates.remove(widget.id)
|
||||
widgetJobs.remove(widget.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onScreenOff() {
|
||||
if (thisSetScope) {
|
||||
widgetScope?.cancel()
|
||||
thisSetScope = false
|
||||
widgetTemplates.clear()
|
||||
widgetJobs.clear()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateAllWidgets(
|
||||
context: Context
|
||||
) {
|
||||
val systemWidgetIds = AppWidgetManager.getInstance(context)
|
||||
.getAppWidgetIds(ComponentName(context, TemplateWidget::class.java))
|
||||
.toSet()
|
||||
val dbWidgetIds = templateWidgetDao.getAll().map { it.id }
|
||||
|
||||
val invalidWidgetIds = dbWidgetIds.minus(systemWidgetIds)
|
||||
if (invalidWidgetIds.isNotEmpty()) {
|
||||
Log.i(TAG, "Found widgets $invalidWidgetIds in database, but not in AppWidgetManager - sending onDeleted")
|
||||
onDeleted(context, invalidWidgetIds.toIntArray())
|
||||
}
|
||||
|
||||
dbWidgetIds.filter { systemWidgetIds.contains(it) }.forEach {
|
||||
updateView(context, it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateView(
|
||||
context: Context,
|
||||
appWidgetId: Int,
|
||||
appWidgetManager: AppWidgetManager = AppWidgetManager.getInstance(context)
|
||||
) {
|
||||
widgetScope?.launch {
|
||||
val views = getWidgetRemoteViews(context, appWidgetId)
|
||||
appWidgetManager.updateAppWidget(appWidgetId, views)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getWidgetRemoteViews(context: Context, appWidgetId: Int, suggestedTemplate: String? = null): RemoteViews {
|
||||
// Every time AppWidgetManager.updateAppWidget(...) is called, the button listener
|
||||
// and label need to be re-assigned, or the next time the layout updates
|
||||
// (e.g home screen rotation) the widget will fall back on its default layout
|
||||
|
@ -97,9 +231,9 @@ class TemplateWidget : BaseWidgetProvider() {
|
|||
}
|
||||
|
||||
// Content
|
||||
var renderedTemplate: String? = templateWidgetDao.get(appWidgetId)?.lastUpdate ?: "Loading"
|
||||
var renderedTemplate: String? = templateWidgetDao.get(appWidgetId)?.lastUpdate ?: context.getString(commonR.string.loading)
|
||||
try {
|
||||
renderedTemplate = integrationUseCase.renderTemplate(widget.template, mapOf()).toString()
|
||||
renderedTemplate = suggestedTemplate ?: integrationUseCase.renderTemplate(widget.template, mapOf()).toString()
|
||||
templateWidgetDao.updateTemplateWidgetLastUpdate(
|
||||
appWidgetId,
|
||||
renderedTemplate
|
||||
|
@ -124,7 +258,7 @@ class TemplateWidget : BaseWidgetProvider() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun saveEntityConfiguration(context: Context, extras: Bundle?, appWidgetId: Int) {
|
||||
private fun saveEntityConfiguration(context: Context, extras: Bundle?, appWidgetId: Int) {
|
||||
if (extras == null) return
|
||||
|
||||
val template: String? = extras.getString(EXTRA_TEMPLATE)
|
||||
|
@ -137,7 +271,7 @@ class TemplateWidget : BaseWidgetProvider() {
|
|||
return
|
||||
}
|
||||
|
||||
mainScope.launch {
|
||||
widgetScope?.launch {
|
||||
templateWidgetDao.add(
|
||||
TemplateWidgetEntity(
|
||||
appWidgetId,
|
||||
|
@ -152,13 +286,10 @@ class TemplateWidget : BaseWidgetProvider() {
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun onEntityStateChanged(context: Context, entity: Entity<*>) {
|
||||
getAllWidgetIds(context).forEach { appWidgetId ->
|
||||
val intent = Intent(context, TemplateWidget::class.java).apply {
|
||||
action = UPDATE_VIEW
|
||||
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
|
||||
}
|
||||
context.sendBroadcast(intent)
|
||||
private fun onTemplateChanged(context: Context, appWidgetId: Int, template: String) {
|
||||
widgetScope?.launch {
|
||||
val views = getWidgetRemoteViews(context, appWidgetId, template)
|
||||
AppWidgetManager.getInstance(context).updateAppWidget(appWidgetId, views)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,6 @@ import io.homeassistant.companion.android.databinding.WidgetTemplateConfigureBin
|
|||
import io.homeassistant.companion.android.settings.widgets.ManageWidgetsViewModel
|
||||
import io.homeassistant.companion.android.util.getHexForColor
|
||||
import io.homeassistant.companion.android.widgets.BaseWidgetConfigureActivity
|
||||
import io.homeassistant.companion.android.widgets.BaseWidgetProvider
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
@ -170,7 +169,7 @@ class TemplateWidgetConfigureActivity : BaseWidgetConfigureActivity() {
|
|||
}
|
||||
|
||||
val createIntent = Intent().apply {
|
||||
action = BaseWidgetProvider.RECEIVE_DATA
|
||||
action = TemplateWidget.RECEIVE_DATA
|
||||
component = ComponentName(applicationContext, TemplateWidget::class.java)
|
||||
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
|
||||
putExtra(TemplateWidget.EXTRA_TEMPLATE, binding.templateText.text.toString())
|
||||
|
|
|
@ -15,6 +15,7 @@ interface IntegrationRepository {
|
|||
suspend fun getNotificationRateLimits(): RateLimitResponse
|
||||
|
||||
suspend fun renderTemplate(template: String, variables: Map<String, String>): String?
|
||||
suspend fun getTemplateUpdates(template: String): Flow<String>?
|
||||
|
||||
suspend fun updateLocation(updateLocation: UpdateLocation)
|
||||
|
||||
|
|
|
@ -178,6 +178,13 @@ class IntegrationRepositoryImpl @Inject constructor(
|
|||
else throw IntegrationException("Error calling integration request render_template")
|
||||
}
|
||||
|
||||
override suspend fun getTemplateUpdates(template: String): Flow<String>? {
|
||||
return webSocketRepository.getTemplateUpdates(template)
|
||||
?.map {
|
||||
it.result
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun updateLocation(updateLocation: UpdateLocation) {
|
||||
val updateLocationRequest = createUpdateLocation(updateLocation)
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import io.homeassistant.companion.android.common.data.websocket.impl.entities.En
|
|||
import io.homeassistant.companion.android.common.data.websocket.impl.entities.EntityRegistryUpdatedEvent
|
||||
import io.homeassistant.companion.android.common.data.websocket.impl.entities.GetConfigResponse
|
||||
import io.homeassistant.companion.android.common.data.websocket.impl.entities.StateChangedEvent
|
||||
import io.homeassistant.companion.android.common.data.websocket.impl.entities.TemplateUpdatedEvent
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface WebSocketRepository {
|
||||
|
@ -25,6 +26,7 @@ interface WebSocketRepository {
|
|||
suspend fun getAreaRegistryUpdates(): Flow<AreaRegistryUpdatedEvent>?
|
||||
suspend fun getDeviceRegistryUpdates(): Flow<DeviceRegistryUpdatedEvent>?
|
||||
suspend fun getEntityRegistryUpdates(): Flow<EntityRegistryUpdatedEvent>?
|
||||
suspend fun getTemplateUpdates(template: String): Flow<TemplateUpdatedEvent>?
|
||||
suspend fun getNotifications(): Flow<Map<String, Any>>?
|
||||
suspend fun ackNotification(confirmId: String): Boolean
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import io.homeassistant.companion.android.common.data.websocket.impl.entities.Ev
|
|||
import io.homeassistant.companion.android.common.data.websocket.impl.entities.GetConfigResponse
|
||||
import io.homeassistant.companion.android.common.data.websocket.impl.entities.SocketResponse
|
||||
import io.homeassistant.companion.android.common.data.websocket.impl.entities.StateChangedEvent
|
||||
import io.homeassistant.companion.android.common.data.websocket.impl.entities.TemplateUpdatedEvent
|
||||
import kotlinx.coroutines.CancellableContinuation
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
@ -63,6 +64,8 @@ class WebSocketRepositoryImpl @Inject constructor(
|
|||
companion object {
|
||||
private const val TAG = "WebSocketRepository"
|
||||
|
||||
private const val SUBSCRIBE_TYPE_SUBSCRIBE_EVENTS = "subscribe_events"
|
||||
private const val SUBSCRIBE_TYPE_RENDER_TEMPLATE = "render_template"
|
||||
private const val EVENT_STATE_CHANGED = "state_changed"
|
||||
private const val EVENT_AREA_REGISTRY_UPDATED = "area_registry_updated"
|
||||
private const val EVENT_DEVICE_REGISTRY_UPDATED = "device_registry_updated"
|
||||
|
@ -82,12 +85,12 @@ class WebSocketRepositoryImpl @Inject constructor(
|
|||
private val connectedMutex = Mutex()
|
||||
private var connected = CompletableDeferred<Boolean>()
|
||||
private val eventSubscriptionMutex = Mutex()
|
||||
private val eventSubscriptionFlow = mutableMapOf<String, SharedFlow<*>>()
|
||||
private var eventSubscriptionProducerScope = mutableMapOf<String, ProducerScope<Any>>()
|
||||
private var eventSubscriptionId = mutableMapOf<Map<Any, Any>, Long?>()
|
||||
private val eventSubscriptionFlow = mutableMapOf<Map<Any, Any>, SharedFlow<*>>()
|
||||
private var eventSubscriptionProducerScope = mutableMapOf<Map<Any, Any>, ProducerScope<Any>>()
|
||||
private val notificationMutex = Mutex()
|
||||
private var notificationFlow: Flow<Map<String, Any>>? = null
|
||||
private var notificationProducerScope: ProducerScope<Map<String, Any>>? = null
|
||||
private var lastResponseID = mutableMapOf<String, Long?>()
|
||||
|
||||
override fun getConnectionState(): WebSocketState? = connectionState
|
||||
|
||||
|
@ -179,46 +182,68 @@ class WebSocketRepositoryImpl @Inject constructor(
|
|||
override suspend fun getEntityRegistryUpdates(): Flow<EntityRegistryUpdatedEvent>? =
|
||||
subscribeToEventsForType(EVENT_ENTITY_REGISTRY_UPDATED)
|
||||
|
||||
private suspend fun <T : Any> subscribeToEventsForType(eventType: String): Flow<T>? {
|
||||
eventSubscriptionMutex.withLock {
|
||||
if (eventSubscriptionFlow[eventType] == null) {
|
||||
private suspend fun <T : Any> subscribeToEventsForType(eventType: String): Flow<T>? =
|
||||
subscribeTo(SUBSCRIBE_TYPE_SUBSCRIBE_EVENTS, mapOf("event_type" to eventType))
|
||||
|
||||
val response = sendMessage(
|
||||
mapOf(
|
||||
"type" to "subscribe_events",
|
||||
"event_type" to eventType
|
||||
)
|
||||
)
|
||||
lastResponseID[eventType] = response?.id
|
||||
if (response == null) {
|
||||
Log.e(TAG, "Unable to register for events of type $eventType")
|
||||
override suspend fun getTemplateUpdates(template: String): Flow<TemplateUpdatedEvent>? =
|
||||
subscribeTo(SUBSCRIBE_TYPE_RENDER_TEMPLATE, mapOf("template" to template))
|
||||
|
||||
/**
|
||||
* Start a subscription for events on the websocket connection and get a Flow for listening to
|
||||
* new messages. When there are no more listeners, the subscription will automatically be cancelled
|
||||
* using `unsubscribe_events`. If the subscription already exists, the existing Flow is returned.
|
||||
*
|
||||
* @param type value for the `type` key in the subscription message, for example `subscribe_events`
|
||||
* @param data a key/value map of additional data to be included in the subscription message, for
|
||||
* example the `event_type` + value when subscribing with `subscribe_events`
|
||||
* @return a Flow that will emit messages delivered to this subscription, or `null` if an error
|
||||
* occurred
|
||||
*/
|
||||
private suspend fun <T : Any> subscribeTo(type: String, data: Map<Any, Any>): Flow<T>? {
|
||||
val subscribeMessage = mapOf(
|
||||
"type" to type
|
||||
).plus(data)
|
||||
|
||||
eventSubscriptionMutex.withLock {
|
||||
if (eventSubscriptionId[subscribeMessage] == null) {
|
||||
|
||||
val response = sendMessage(subscribeMessage)
|
||||
if (response == null || response.success != true) {
|
||||
Log.e(TAG, "Unable to subscribe to $type with data $data")
|
||||
return null
|
||||
} else {
|
||||
eventSubscriptionId[subscribeMessage] = response.id
|
||||
}
|
||||
|
||||
eventSubscriptionFlow[eventType] = callbackFlow<T> {
|
||||
eventSubscriptionProducerScope[eventType] = this as ProducerScope<Any>
|
||||
// Subscriptions are stored by subscribe message instead of ID, because the ID will
|
||||
// change when the app needs to resubscribe
|
||||
eventSubscriptionFlow[subscribeMessage] = callbackFlow<T> {
|
||||
eventSubscriptionProducerScope[subscribeMessage] = this as ProducerScope<Any>
|
||||
awaitClose {
|
||||
if (lastResponseID[eventType] != null) {
|
||||
Log.d(TAG, "Unsubscribing from $eventType")
|
||||
if (eventSubscriptionId[subscribeMessage] != null) {
|
||||
Log.d(TAG, "Unsubscribing from $type with data $data")
|
||||
ioScope.launch {
|
||||
sendMessage(
|
||||
mapOf(
|
||||
"type" to "unsubscribe_events",
|
||||
"subscription" to lastResponseID[eventType]
|
||||
"subscription" to eventSubscriptionId[subscribeMessage]!!
|
||||
)
|
||||
)
|
||||
lastResponseID.remove(eventType)
|
||||
eventSubscriptionId.remove(subscribeMessage)
|
||||
}
|
||||
}
|
||||
eventSubscriptionProducerScope.remove(eventType)
|
||||
eventSubscriptionFlow.remove(eventType)
|
||||
eventSubscriptionProducerScope.remove(subscribeMessage)
|
||||
eventSubscriptionFlow.remove(subscribeMessage)
|
||||
}
|
||||
}.shareIn(ioScope, SharingStarted.WhileSubscribed())
|
||||
}
|
||||
}
|
||||
return eventSubscriptionFlow[eventType]!! as Flow<T>
|
||||
return eventSubscriptionFlow[subscribeMessage]!! as Flow<T>
|
||||
}
|
||||
|
||||
private fun getSubscriptionMessageById(id: Long): Map<Any, Any>? =
|
||||
eventSubscriptionId.filterValues { it == id }.keys.firstOrNull()
|
||||
|
||||
override suspend fun getNotifications(): Flow<Map<String, Any>>? {
|
||||
notificationMutex.withLock {
|
||||
if (notificationFlow == null) {
|
||||
|
@ -355,10 +380,20 @@ class WebSocketRepositoryImpl @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun handleEvent(response: SocketResponse) {
|
||||
val subscriptionId = response.id
|
||||
if (subscriptionId != null && eventSubscriptionId.values.contains(subscriptionId)) {
|
||||
val subscriptionMessage = getSubscriptionMessageById(subscriptionId)
|
||||
val subscriptionType = subscriptionMessage?.get("type")
|
||||
val eventResponseType = response.event?.get("event_type")
|
||||
if (eventResponseType != null && eventResponseType.isTextual) {
|
||||
|
||||
val message: Any =
|
||||
if (subscriptionType == SUBSCRIBE_TYPE_RENDER_TEMPLATE) {
|
||||
mapper.convertValue(response.event, TemplateUpdatedEvent::class.java)
|
||||
} else if (eventResponseType != null && eventResponseType.isTextual) {
|
||||
val eventResponseClass = when (eventResponseType.textValue()) {
|
||||
EVENT_STATE_CHANGED -> object : TypeReference<EventResponse<StateChangedEvent>>() {}
|
||||
EVENT_STATE_CHANGED ->
|
||||
object :
|
||||
TypeReference<EventResponse<StateChangedEvent>>() {}
|
||||
EVENT_AREA_REGISTRY_UPDATED ->
|
||||
object :
|
||||
TypeReference<EventResponse<AreaRegistryUpdatedEvent>>() {}
|
||||
|
@ -373,11 +408,17 @@ class WebSocketRepositoryImpl @Inject constructor(
|
|||
object : TypeReference<EventResponse<Any>>() {}
|
||||
}
|
||||
}
|
||||
val eventResponse = mapper.convertValue(
|
||||
|
||||
mapper.convertValue(
|
||||
response.event,
|
||||
eventResponseClass
|
||||
)
|
||||
eventSubscriptionProducerScope[eventResponse.eventType]?.send(eventResponse.data)
|
||||
).data
|
||||
} else {
|
||||
Log.d(TAG, "Unknown event for subscription received, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
eventSubscriptionProducerScope[subscriptionMessage]?.send(message)
|
||||
} else if (response.event?.contains("hass_confirm_id") == true) {
|
||||
if (notificationProducerScope?.isActive == true) {
|
||||
notificationProducerScope?.send(
|
||||
|
@ -404,16 +445,13 @@ class WebSocketRepositoryImpl @Inject constructor(
|
|||
ioScope.launch {
|
||||
delay(10000)
|
||||
if (connect()) {
|
||||
eventSubscriptionFlow.forEach { (eventType, _) ->
|
||||
val response = sendMessage(
|
||||
mapOf(
|
||||
"type" to "subscribe_events",
|
||||
"event_type" to eventType
|
||||
)
|
||||
)
|
||||
lastResponseID[eventType] = response?.id
|
||||
if (response == null) {
|
||||
Log.e(TAG, "Issue re-registering event subscriptions")
|
||||
eventSubscriptionFlow.forEach { (subscribeMessage, _) ->
|
||||
val response = sendMessage(subscribeMessage)
|
||||
if (response == null || response.success != true) {
|
||||
Log.e(TAG, "Issue re-registering subscription with $subscribeMessage")
|
||||
eventSubscriptionId[subscribeMessage] = null
|
||||
} else {
|
||||
eventSubscriptionId[subscribeMessage] = response.id
|
||||
}
|
||||
}
|
||||
if (notificationFlow != null) {
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package io.homeassistant.companion.android.common.data.websocket.impl.entities
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class TemplateUpdatedEvent(
|
||||
val result: String,
|
||||
val listeners: Map<String, Any>
|
||||
)
|
Loading…
Reference in a new issue