Skip to content

Commit

Permalink
Merge branch 'master' into HA_Entity_Loading
Browse files Browse the repository at this point in the history
  • Loading branch information
googlvalenzuela authored May 15, 2023
2 parents 7dcbcb8 + d81913b commit 496cd10
Show file tree
Hide file tree
Showing 14 changed files with 210 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -947,7 +947,7 @@ class LocationSensorManager : LocationSensorManagerBase() {
val configuredZones = getZones(serverId, forceRefresh = true)
configuredZones.forEach {
addGeofenceToBuilder(geofencingRequestBuilder, serverId, it)
if (highAccuracyTriggerRange > 0 && highAccuracyZones.contains(it.entityId)) {
if (highAccuracyTriggerRange > 0 && highAccuracyZones.contains("${serverId}_${it.entityId}")) {
addGeofenceToBuilder(geofencingRequestBuilder, serverId, it, highAccuracyTriggerRange)
}
}
Expand All @@ -963,7 +963,7 @@ class LocationSensorManager : LocationSensorManagerBase() {
zone: Entity<ZoneAttributes>,
triggerRange: Int = 0
) {
val postRequestId = if (triggerRange > 0)"_expanded" else ""
val postRequestId = if (triggerRange > 0) "_expanded" else ""
geofencingRequestBuilder
.addGeofence(
Geofence.Builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ class SensorDetailViewModel @Inject constructor(
.filter { entries == null || entries.contains(it) }
.map {
val server = servers.first { s -> s.id == it.split("_")[0].toInt() }
val zone = it.split("_")[1]
val zone = it.split("_", limit = 2)[1]
if (servers.size > 1) "${server.friendlyName}: $zone" else zone
}
val entriesNotInZones = entries
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import androidx.core.graphics.toColorInt
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.google.android.material.color.DynamicColors
import com.maltaisn.icondialog.IconDialog
import com.maltaisn.icondialog.IconDialogSettings
Expand Down Expand Up @@ -134,8 +135,24 @@ class ButtonWidgetConfigureActivity : BaseWidgetConfigureActivity(), IconDialog.
val fieldKeys = fields.keys
Log.d(TAG, "Fields applicable to this service: $fields")

if (target !== false) {
dynamicFields.add(0, ServiceFieldBinder(serviceText, "entity_id"))
val existingServiceData = mutableMapOf<String, Any?>()
buttonWidgetDao.get(appWidgetId)?.let { buttonWidget ->
if (
buttonWidget.serverId != selectedServerId ||
"${buttonWidget.domain}.${buttonWidget.service}" != serviceText
) {
return@let
}

val dbMap: HashMap<String, Any?> = jacksonObjectMapper().readValue(buttonWidget.serviceData)
for (item in dbMap) {
val value = item.value.toString().replace("[", "").replace("]", "") + if (item.key == "entity_id") ", " else ""
existingServiceData[item.key] = value.ifEmpty { null }
}
}

if (target != false) {
dynamicFields.add(0, ServiceFieldBinder(serviceText, "entity_id", existingServiceData["entity_id"]))
}

fieldKeys.sorted().forEach { fieldKey ->
Expand All @@ -145,9 +162,9 @@ class ButtonWidgetConfigureActivity : BaseWidgetConfigureActivity(), IconDialog.
// IDs get priority and go at the top, since the other fields
// are usually optional but the ID is required
if (fieldKey.contains("_id")) {
dynamicFields.add(0, ServiceFieldBinder(serviceText, fieldKey))
dynamicFields.add(0, ServiceFieldBinder(serviceText, fieldKey, existingServiceData[fieldKey]))
} else {
dynamicFields.add(ServiceFieldBinder(serviceText, fieldKey))
dynamicFields.add(ServiceFieldBinder(serviceText, fieldKey, existingServiceData[fieldKey]))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ interface IntegrationRepository {

suspend fun shouldNotifySecurityWarning(): Boolean

suspend fun getConversation(speech: String): String?
suspend fun getAssistResponse(speech: String): String?
}

@AssistedFactory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,21 @@ import io.homeassistant.companion.android.common.data.integration.impl.entities.
import io.homeassistant.companion.android.common.data.integration.impl.entities.Template
import io.homeassistant.companion.android.common.data.integration.impl.entities.UpdateLocationRequest
import io.homeassistant.companion.android.common.data.servers.ServerManager
import io.homeassistant.companion.android.common.data.websocket.impl.entities.AssistPipelineEventType
import io.homeassistant.companion.android.common.data.websocket.impl.entities.AssistPipelineIntentEnd
import io.homeassistant.companion.android.common.data.websocket.impl.entities.GetConfigResponse
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import java.util.concurrent.TimeUnit
import javax.inject.Named
import kotlin.coroutines.resume

class IntegrationRepositoryImpl @AssistedInject constructor(
private val integrationService: IntegrationService,
Expand Down Expand Up @@ -64,6 +72,8 @@ class IntegrationRepositoryImpl @AssistedInject constructor(
private const val APPLOCK_TIMEOUT_GRACE_MS = 1000
}

private val ioScope = CoroutineScope(Dispatchers.IO + Job())

private val server get() = serverManager.getServer(serverId)!!

private val webSocketRepository get() = serverManager.webSocketRepository(serverId)
Expand Down Expand Up @@ -377,7 +387,7 @@ class IntegrationRepositoryImpl @AssistedInject constructor(
}

override suspend fun setAppActive(active: Boolean) {
if (!active) {
if (!active && appActive) {
setSessionExpireMillis(System.currentTimeMillis() + (getSessionTimeOut() * 1000) + APPLOCK_TIMEOUT_GRACE_MS)
}
Log.d(TAG, "setAppActive(): $active")
Expand Down Expand Up @@ -523,11 +533,29 @@ class IntegrationRepositoryImpl @AssistedInject constructor(
}?.toList()
}

override suspend fun getConversation(speech: String): String? {
// TODO: Also send back conversation ID for dialogue
val response = webSocketRepository.getConversation(speech)

return response?.response?.speech?.plain?.get("speech")
override suspend fun getAssistResponse(speech: String): String? {
return if (server.version?.isAtLeast(2023, 5, 0) == true) {
var job: Job? = null
val response = suspendCancellableCoroutine { cont ->
job = ioScope.launch {
webSocketRepository.runAssistPipeline(speech)?.collect {
if (!cont.isActive) return@collect
when (it.type) {
AssistPipelineEventType.INTENT_END ->
cont.resume((it.data as AssistPipelineIntentEnd).intentOutput.response.speech.plain["speech"])
AssistPipelineEventType.ERROR,
AssistPipelineEventType.RUN_END -> cont.resume(null)
else -> { /* Do nothing */ }
}
} ?: cont.resume(null)
}
}
job?.cancel()
response
} else {
val response = webSocketRepository.getConversation(speech)
response?.response?.speech?.plain?.get("speech")
}
}

override suspend fun getEntities(): List<Entity<Any>>? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import io.homeassistant.companion.android.common.data.integration.impl.entities.
import io.homeassistant.companion.android.common.data.websocket.impl.WebSocketRepositoryImpl
import io.homeassistant.companion.android.common.data.websocket.impl.entities.AreaRegistryResponse
import io.homeassistant.companion.android.common.data.websocket.impl.entities.AreaRegistryUpdatedEvent
import io.homeassistant.companion.android.common.data.websocket.impl.entities.AssistPipelineEvent
import io.homeassistant.companion.android.common.data.websocket.impl.entities.CompressedStateChangedEvent
import io.homeassistant.companion.android.common.data.websocket.impl.entities.ConversationResponse
import io.homeassistant.companion.android.common.data.websocket.impl.entities.CurrentUserResponse
Expand Down Expand Up @@ -48,7 +49,18 @@ interface WebSocketRepository {
suspend fun getThreadDatasets(): List<ThreadDatasetResponse>?
suspend fun getThreadDatasetTlv(datasetId: String): ThreadDatasetTlvResponse?
suspend fun addThreadDataset(tlv: ByteArray): Boolean

/**
* Get an Assist response for the given text input. For core >= 2023.5, use [runAssistPipeline]
* instead.
*/
suspend fun getConversation(speech: String): ConversationResponse?

/**
* Run the Assist pipeline for the given text input
* @return a Flow that will emit all events for the pipeline
*/
suspend fun runAssistPipeline(text: String): Flow<AssistPipelineEvent>?
}

@AssistedFactory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ import io.homeassistant.companion.android.common.data.websocket.WebSocketRequest
import io.homeassistant.companion.android.common.data.websocket.WebSocketState
import io.homeassistant.companion.android.common.data.websocket.impl.entities.AreaRegistryResponse
import io.homeassistant.companion.android.common.data.websocket.impl.entities.AreaRegistryUpdatedEvent
import io.homeassistant.companion.android.common.data.websocket.impl.entities.AssistPipelineEvent
import io.homeassistant.companion.android.common.data.websocket.impl.entities.AssistPipelineEventType
import io.homeassistant.companion.android.common.data.websocket.impl.entities.AssistPipelineIntentEnd
import io.homeassistant.companion.android.common.data.websocket.impl.entities.AssistPipelineIntentStart
import io.homeassistant.companion.android.common.data.websocket.impl.entities.AssistPipelineRunStart
import io.homeassistant.companion.android.common.data.websocket.impl.entities.CompressedStateChangedEvent
import io.homeassistant.companion.android.common.data.websocket.impl.entities.ConversationResponse
import io.homeassistant.companion.android.common.data.websocket.impl.entities.CurrentUserResponse
Expand Down Expand Up @@ -81,6 +86,7 @@ class WebSocketRepositoryImpl @AssistedInject constructor(
companion object {
private const val TAG = "WebSocketRepository"

private const val SUBSCRIBE_TYPE_ASSIST_PIPELINE_RUN = "assist_pipeline/run"
private const val SUBSCRIBE_TYPE_SUBSCRIBE_EVENTS = "subscribe_events"
private const val SUBSCRIBE_TYPE_SUBSCRIBE_ENTITIES = "subscribe_entities"
private const val SUBSCRIBE_TYPE_SUBSCRIBE_TRIGGER = "subscribe_trigger"
Expand Down Expand Up @@ -209,6 +215,18 @@ class WebSocketRepositoryImpl @AssistedInject constructor(
return mapResponse(socketResponse)
}

override suspend fun runAssistPipeline(text: String): Flow<AssistPipelineEvent>? =
subscribeTo(
SUBSCRIBE_TYPE_ASSIST_PIPELINE_RUN,
mapOf(
"start_stage" to "intent",
"end_stage" to "intent",
"input" to mapOf(
"text" to text
)
)
)

override suspend fun getStateChanges(): Flow<StateChangedEvent>? =
subscribeToEventsForType(EVENT_STATE_CHANGED)

Expand Down Expand Up @@ -629,6 +647,21 @@ class WebSocketRepositoryImpl @AssistedInject constructor(
Log.w(TAG, "Received no trigger value for trigger subscription, skipping")
return
}
} else if (subscriptionType == SUBSCRIBE_TYPE_ASSIST_PIPELINE_RUN) {
val eventType = response.event?.get("type")
if (eventType?.isTextual == true) {
val eventDataMap = response.event.get("data")
val eventData = when (eventType.textValue()) {
AssistPipelineEventType.RUN_START -> mapper.convertValue(eventDataMap, AssistPipelineRunStart::class.java)
AssistPipelineEventType.INTENT_START -> mapper.convertValue(eventDataMap, AssistPipelineIntentStart::class.java)
AssistPipelineEventType.INTENT_END -> mapper.convertValue(eventDataMap, AssistPipelineIntentEnd::class.java)
else -> null
}
AssistPipelineEvent(eventType.textValue(), eventData)
} else {
Log.w(TAG, "Received Assist pipeline event without type, skipping")
return
}
} else if (eventResponseType != null && eventResponseType.isTextual) {
val eventResponseClass = when (eventResponseType.textValue()) {
EVENT_STATE_CHANGED ->
Expand Down Expand Up @@ -737,17 +770,18 @@ class WebSocketRepositoryImpl @AssistedInject constructor(
listOf(mapper.readValue(text))
}

messages.forEach { message ->
Log.d(TAG, "Message number ${message.id} received")

messages.groupBy { it.id }.values.forEach { messagesForId ->
ioScope.launch {
when (message.type) {
"auth_required" -> Log.d(TAG, "Auth Requested")
"auth_ok" -> handleAuthComplete(true, message.haVersion)
"auth_invalid" -> handleAuthComplete(false, message.haVersion)
"pong", "result" -> handleMessage(message)
"event" -> handleEvent(message)
else -> Log.d(TAG, "Unknown message type: ${message.type}")
messagesForId.forEach { message ->
Log.d(TAG, "Message number ${message.id} received")
when (message.type) {
"auth_required" -> Log.d(TAG, "Auth Requested")
"auth_ok" -> handleAuthComplete(true, message.haVersion)
"auth_invalid" -> handleAuthComplete(false, message.haVersion)
"pong", "result" -> handleMessage(message)
"event" -> handleEvent(message)
else -> Log.d(TAG, "Unknown message type: ${message.type}")
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.homeassistant.companion.android.common.data.websocket.impl.entities

import com.fasterxml.jackson.annotation.JsonIgnoreProperties

data class AssistPipelineEvent(
val type: String,
val data: AssistPipelineEventData?
)

object AssistPipelineEventType {
const val RUN_START = "run-start"
const val RUN_END = "run-end"
const val STT_START = "stt-start"
const val STT_END = "stt-end"
const val INTENT_START = "intent-start"
const val INTENT_END = "intent-end"
const val TTS_START = "tts-start"
const val TTS_END = "tts-end"
const val ERROR = "error"
}

interface AssistPipelineEventData

@JsonIgnoreProperties(ignoreUnknown = true)
data class AssistPipelineRunStart(
val pipeline: String,
val language: String,
val runnerData: Map<String, Any?>
) : AssistPipelineEventData

@JsonIgnoreProperties(ignoreUnknown = true)
data class AssistPipelineIntentStart(
val engine: String,
val language: String,
val intentInput: String
) : AssistPipelineEventData

@JsonIgnoreProperties(ignoreUnknown = true)
data class AssistPipelineIntentEnd(
val intentOutput: ConversationResponse
) : AssistPipelineEventData
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.homeassistant.companion.android.util

import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.transform

/**
* Emit the first value from a Flow, and then after the period has passed the last item emitted
* by the Flow during that period (if different). This ensures throttling/debouncing but also
* always receiving the first and last item of the Flow.
*
* From https://github.com/Kotlin/kotlinx.coroutines/issues/1446#issuecomment-1198103541
*/
fun <T> Flow<T>.throttleLatest(delayMillis: Long): Flow<T> = this
.conflate()
.transform {
emit(it)
delay(delayMillis)
}
4 changes: 3 additions & 1 deletion common/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1052,7 +1052,9 @@
<string name="tile_vibrate">Vibrate when clicked</string>
<string name="tile_auth_required">Requires unlocked device</string>
<string name="no_results">No results yet</string>
<string name="no_conversation_support">You must be at least on Home Assistant 2023.1 and have the conversation integration enabled</string>
<string name="no_assist_support">You must be at least on Home Assistant %1$s and have the %2$s integration enabled</string>
<string name="no_assist_support_conversation">conversation</string>
<string name="no_assist_support_assist_pipeline">Assist pipeline</string>
<string name="conversation">Conversation</string>
<string name="assist">Assist</string>
<string name="assist_log_in">Log in to Home Assistant to start using Assist</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ class ConversationActivity : ComponentActivity() {
super.onCreate(savedInstanceState)

lifecycleScope.launch {
conversationViewModel.isSupportConversation()
if (conversationViewModel.supportsConversation) {
conversationViewModel.checkAssistSupport()
if (conversationViewModel.supportsAssist) {
val searchIntent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
putExtra(
RecognizerIntent.EXTRA_LANGUAGE_MODEL,
Expand Down
Loading

0 comments on commit 496cd10

Please sign in to comment.