diff --git a/app/src/main/java/dev/coffeeco/homenative/MainActivity.kt b/app/src/main/java/dev/coffeeco/homenative/MainActivity.kt index a52b2a4..f22e17c 100644 --- a/app/src/main/java/dev/coffeeco/homenative/MainActivity.kt +++ b/app/src/main/java/dev/coffeeco/homenative/MainActivity.kt @@ -21,6 +21,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberCoroutineScope @@ -29,13 +30,16 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.lifecycleScope import dev.coffeeco.homenative.data.HomeClient import dev.coffeeco.homenative.data.obtainClient import dev.coffeeco.homenative.data.setInstance import dev.coffeeco.homenative.ui.ControlContainer import dev.coffeeco.homenative.ui.SignInUI import dev.coffeeco.homenative.ui.theme.HomeNativeTheme +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking class MainActivity : ComponentActivity() { @@ -55,11 +59,13 @@ class MainActivity : ComponentActivity() { @Composable private fun AppContainer() { val ctx = LocalContext.current - if (homeClient == null) { - val cf = obtainClient(ctx) - runBlocking { - homeClient = cf.first() - homeClient!!.wsClient.connect() + LaunchedEffect(Unit) { + if (homeClient == null) { + val cf = obtainClient(ctx) + lifecycleScope.launch(Dispatchers.IO) { + homeClient = cf.first() + homeClient!!.wsClient.connect() + } } } diff --git a/app/src/main/java/dev/coffeeco/homenative/data/HomeClient.kt b/app/src/main/java/dev/coffeeco/homenative/data/HomeClient.kt index 116fc97..407d653 100644 --- a/app/src/main/java/dev/coffeeco/homenative/data/HomeClient.kt +++ b/app/src/main/java/dev/coffeeco/homenative/data/HomeClient.kt @@ -5,12 +5,7 @@ import io.ktor.client.call.body import io.ktor.client.engine.okhttp.OkHttp import io.ktor.client.plugins.DefaultRequest import io.ktor.client.plugins.contentnegotiation.ContentNegotiation -import io.ktor.client.plugins.websocket.DefaultClientWebSocketSession import io.ktor.client.plugins.websocket.WebSockets -import io.ktor.client.plugins.websocket.receiveDeserialized -import io.ktor.client.plugins.websocket.sendSerialized -import io.ktor.client.plugins.websocket.webSocket -import io.ktor.client.plugins.websocket.webSocketSession import io.ktor.client.request.get import io.ktor.client.request.header import io.ktor.client.request.post @@ -21,33 +16,21 @@ import io.ktor.http.HttpHeaders import io.ktor.http.contentType import io.ktor.serialization.kotlinx.KotlinxWebsocketSerializationConverter import io.ktor.serialization.kotlinx.json.json -import io.ktor.utils.io.InternalAPI -import io.ktor.websocket.DefaultWebSocketSession -import io.ktor.websocket.FrameType -import io.ktor.websocket.send -import io.ktor.websocket.serialization.receiveDeserializedBase -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch import kotlinx.serialization.json.Json -import java.util.EventListener class HomeClient(private val baseUrl: String, private val accessToken: String) { - val jsonConfig = Json { // isLenient = true - // useArrayPolymorphism = true - allowStructuredMapKeys = true - ignoreUnknownKeys = true // explicitNulls = false - // namingStrategy = JsonNamingStrategy.SnakeCase - encodeDefaults = true // IMPORTANT - } - private val client: HttpClient = HttpClient(OkHttp) { install(WebSockets) { contentConverter = KotlinxWebsocketSerializationConverter(Json) } install(ContentNegotiation) { - json(jsonConfig) + json(Json { // isLenient = true + // useArrayPolymorphism = true + allowStructuredMapKeys = true + ignoreUnknownKeys = true // explicitNulls = false + // namingStrategy = JsonNamingStrategy.SnakeCase + encodeDefaults = true + }) } install(DefaultRequest) { @@ -55,7 +38,7 @@ class HomeClient(private val baseUrl: String, private val accessToken: String) { } } - val wsClient = WSClient(GlobalScope, baseUrl, accessToken, client) + val wsClient = WSClient(baseUrl, accessToken, client) suspend fun apiStatus(): HttpResponse { val res = client.get("$baseUrl/api/") diff --git a/app/src/main/java/dev/coffeeco/homenative/data/HttpRequests.kt b/app/src/main/java/dev/coffeeco/homenative/data/HttpRequests.kt new file mode 100644 index 0000000..583ed30 --- /dev/null +++ b/app/src/main/java/dev/coffeeco/homenative/data/HttpRequests.kt @@ -0,0 +1,9 @@ +package dev.coffeeco.homenative.data + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class PostServiceBody( + @SerialName("entity_id") val entityId: String +) diff --git a/app/src/main/java/dev/coffeeco/homenative/data/Responses.kt b/app/src/main/java/dev/coffeeco/homenative/data/HttpResponses.kt similarity index 59% rename from app/src/main/java/dev/coffeeco/homenative/data/Responses.kt rename to app/src/main/java/dev/coffeeco/homenative/data/HttpResponses.kt index 8134f07..54653f2 100644 --- a/app/src/main/java/dev/coffeeco/homenative/data/Responses.kt +++ b/app/src/main/java/dev/coffeeco/homenative/data/HttpResponses.kt @@ -2,7 +2,6 @@ package dev.coffeeco.homenative.data import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonObject @Serializable data class AuthRes( @@ -45,50 +44,3 @@ data class StatesResItemContext( @SerialName("parent_id") val parentId: String?, @SerialName("user_id") val userId: String? ) - -@Serializable -sealed class WebSocketIncoming { - @SerialName("type") - abstract val type: String -} - -@Serializable -@SerialName("auth_required") -data class WebSocketAuthRequired( - @SerialName("ha_version") - val homeAssistantVersion: String, -) : WebSocketIncoming() { - override val type: String - get() = "auth_required" -} - -@Serializable -@SerialName("auth_ok") -data class WebSocketAuthOk( - @SerialName("ha_version") - val homeAssistantVersion: String -) : WebSocketIncoming() { - override val type: String - get() = "auth_ok" -} - -@Serializable -@SerialName("auth_invalid") -data class WebSocketAuthInvalid( - val message: String -) : WebSocketIncoming() { - override val type: String - get() = "auth_invalid" -} - -@Serializable -@SerialName("result") -data class WebSocketResult( - val id: Int, - val success: Boolean, - val result: JsonObject? = null, - val error: JsonObject? = null -) : WebSocketIncoming() { - override val type: String - get() = "result" -} diff --git a/app/src/main/java/dev/coffeeco/homenative/data/WSClient.kt b/app/src/main/java/dev/coffeeco/homenative/data/WSClient.kt index 63bd2c4..19e242f 100644 --- a/app/src/main/java/dev/coffeeco/homenative/data/WSClient.kt +++ b/app/src/main/java/dev/coffeeco/homenative/data/WSClient.kt @@ -6,14 +6,10 @@ import io.ktor.client.plugins.websocket.sendSerialized import io.ktor.client.plugins.websocket.webSocket import io.ktor.websocket.DefaultWebSocketSession import io.ktor.websocket.send -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json class WSClient( - private val coroutineScope: CoroutineScope, private val baseUrl: String, private val accessToken: String, private val httpClient: HttpClient, @@ -23,34 +19,31 @@ class WSClient( private var iteratingNumber = 0 suspend fun connect() { - GlobalScope.launch { - httpClient.webSocket(wsUrl) { - session = this - while (true) { - try { - val data = receiveDeserialized() - when (data.type) { - "auth_required" -> { - println("WebSocket: Authentication required") - sendSerialized(WSAuth("auth", accessToken)) - } - - "auth_ok" -> { - println("WebSocket: Authentication OK") - val data1 = WSSubscribe(iteratingNumber) - val serialized = Json.encodeToString(data1) -// send(serialized) - sendSerialized(data1) - } - - else -> { - println("WebSocket: Misc data received: $data") - } + httpClient.webSocket(wsUrl) { + session = this + while (true) { + try { + val data = receiveDeserialized() + when (data.type) { + "auth_required" -> { + println("WebSocket: Authentication required") + sendSerialized(WSAuth("auth", accessToken)) + } + + "auth_ok" -> { + println("WebSocket: Authentication OK") + val data1 = WSSubscribe(iteratingNumber) + val serialized = Json.encodeToString(data1) + sendSerialized(data1) + } + + else -> { + println("WebSocket: Misc data received: $data") } - iteratingNumber++ - } catch (e: Exception) { - println("WS Exception: $e") } + iteratingNumber++ + } catch (e: Exception) { + println("WS Exception: $e") } } } diff --git a/app/src/main/java/dev/coffeeco/homenative/data/WSIncoming.kt b/app/src/main/java/dev/coffeeco/homenative/data/WSIncoming.kt new file mode 100644 index 0000000..b0dfa07 --- /dev/null +++ b/app/src/main/java/dev/coffeeco/homenative/data/WSIncoming.kt @@ -0,0 +1,52 @@ +package dev.coffeeco.homenative.data + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonObject + +@Serializable +sealed class WebSocketIncoming { + @SerialName("type") + abstract val type: String +} + +@Serializable +@SerialName("auth_required") +data class WebSocketAuthRequired( + @SerialName("ha_version") + val homeAssistantVersion: String, +) : WebSocketIncoming() { + override val type: String + get() = "auth_required" +} + +@Serializable +@SerialName("auth_ok") +data class WebSocketAuthOk( + @SerialName("ha_version") + val homeAssistantVersion: String +) : WebSocketIncoming() { + override val type: String + get() = "auth_ok" +} + +@Serializable +@SerialName("auth_invalid") +data class WebSocketAuthInvalid( + val message: String +) : WebSocketIncoming() { + override val type: String + get() = "auth_invalid" +} + +@Serializable +@SerialName("result") +data class WebSocketResult( + val id: Int, + val success: Boolean, + val result: JsonObject? = null, + val error: JsonObject? = null +) : WebSocketIncoming() { + override val type: String + get() = "result" +} diff --git a/app/src/main/java/dev/coffeeco/homenative/data/Requests.kt b/app/src/main/java/dev/coffeeco/homenative/data/WSOutgoing.kt similarity index 63% rename from app/src/main/java/dev/coffeeco/homenative/data/Requests.kt rename to app/src/main/java/dev/coffeeco/homenative/data/WSOutgoing.kt index 3156964..f096dd4 100644 --- a/app/src/main/java/dev/coffeeco/homenative/data/Requests.kt +++ b/app/src/main/java/dev/coffeeco/homenative/data/WSOutgoing.kt @@ -4,11 +4,6 @@ import kotlinx.serialization.EncodeDefault import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -@Serializable -data class PostServiceBody( - @SerialName("entity_id") val entityId: String -) - @Serializable /** * Special since `id` field must be omitted in contrast to [WebSocketMessage.id] @@ -22,11 +17,14 @@ data class WSAuth( ) @Serializable -sealed class WebSocketMessage { +/** + * Generic format for outgoing messages + */ +sealed class WebSocketMessage( @EncodeDefault @SerialName("type") - abstract val type: String - + val type: String, +) { @SerialName("id") abstract val id: Int } @@ -35,13 +33,6 @@ sealed class WebSocketMessage { data class WSSubscribe( @SerialName("id") override val id: Int, - // @EncodeDefault - // @SerialName("type") - // val type: String = "subscribe_events", @SerialName("event_type") val eventType: String? = null, -) : WebSocketMessage() { - @EncodeDefault - @SerialName("type") - override val type: String = "subscribe_events" -} +) : WebSocketMessage(type = "subscribe_events")