test??
This commit is contained in:
parent
fc1a8e0367
commit
89ea5df36e
7 changed files with 110 additions and 124 deletions
|
|
@ -21,6 +21,7 @@ import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
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.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import dev.coffeeco.homenative.data.HomeClient
|
import dev.coffeeco.homenative.data.HomeClient
|
||||||
import dev.coffeeco.homenative.data.obtainClient
|
import dev.coffeeco.homenative.data.obtainClient
|
||||||
import dev.coffeeco.homenative.data.setInstance
|
import dev.coffeeco.homenative.data.setInstance
|
||||||
import dev.coffeeco.homenative.ui.ControlContainer
|
import dev.coffeeco.homenative.ui.ControlContainer
|
||||||
import dev.coffeeco.homenative.ui.SignInUI
|
import dev.coffeeco.homenative.ui.SignInUI
|
||||||
import dev.coffeeco.homenative.ui.theme.HomeNativeTheme
|
import dev.coffeeco.homenative.ui.theme.HomeNativeTheme
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
|
@ -55,11 +59,13 @@ class MainActivity : ComponentActivity() {
|
||||||
@Composable
|
@Composable
|
||||||
private fun AppContainer() {
|
private fun AppContainer() {
|
||||||
val ctx = LocalContext.current
|
val ctx = LocalContext.current
|
||||||
if (homeClient == null) {
|
LaunchedEffect(Unit) {
|
||||||
val cf = obtainClient(ctx)
|
if (homeClient == null) {
|
||||||
runBlocking {
|
val cf = obtainClient(ctx)
|
||||||
homeClient = cf.first()
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
homeClient!!.wsClient.connect()
|
homeClient = cf.first()
|
||||||
|
homeClient!!.wsClient.connect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,7 @@ import io.ktor.client.call.body
|
||||||
import io.ktor.client.engine.okhttp.OkHttp
|
import io.ktor.client.engine.okhttp.OkHttp
|
||||||
import io.ktor.client.plugins.DefaultRequest
|
import io.ktor.client.plugins.DefaultRequest
|
||||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
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.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.get
|
||||||
import io.ktor.client.request.header
|
import io.ktor.client.request.header
|
||||||
import io.ktor.client.request.post
|
import io.ktor.client.request.post
|
||||||
|
|
@ -21,33 +16,21 @@ import io.ktor.http.HttpHeaders
|
||||||
import io.ktor.http.contentType
|
import io.ktor.http.contentType
|
||||||
import io.ktor.serialization.kotlinx.KotlinxWebsocketSerializationConverter
|
import io.ktor.serialization.kotlinx.KotlinxWebsocketSerializationConverter
|
||||||
import io.ktor.serialization.kotlinx.json.json
|
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 kotlinx.serialization.json.Json
|
||||||
import java.util.EventListener
|
|
||||||
|
|
||||||
class HomeClient(private val baseUrl: String, private val accessToken: String) {
|
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) {
|
private val client: HttpClient = HttpClient(OkHttp) {
|
||||||
install(WebSockets) {
|
install(WebSockets) {
|
||||||
contentConverter = KotlinxWebsocketSerializationConverter(Json)
|
contentConverter = KotlinxWebsocketSerializationConverter(Json)
|
||||||
}
|
}
|
||||||
install(ContentNegotiation) {
|
install(ContentNegotiation) {
|
||||||
json(jsonConfig)
|
json(Json { // isLenient = true
|
||||||
|
// useArrayPolymorphism = true
|
||||||
|
allowStructuredMapKeys = true
|
||||||
|
ignoreUnknownKeys = true // explicitNulls = false
|
||||||
|
// namingStrategy = JsonNamingStrategy.SnakeCase
|
||||||
|
encodeDefaults = true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
install(DefaultRequest) {
|
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 {
|
suspend fun apiStatus(): HttpResponse {
|
||||||
val res = client.get("$baseUrl/api/")
|
val res = client.get("$baseUrl/api/")
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
@ -2,7 +2,6 @@ package dev.coffeeco.homenative.data
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.JsonObject
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class AuthRes(
|
data class AuthRes(
|
||||||
|
|
@ -45,50 +44,3 @@ data class StatesResItemContext(
|
||||||
@SerialName("parent_id") val parentId: String?,
|
@SerialName("parent_id") val parentId: String?,
|
||||||
@SerialName("user_id") val userId: 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"
|
|
||||||
}
|
|
||||||
|
|
@ -6,14 +6,10 @@ import io.ktor.client.plugins.websocket.sendSerialized
|
||||||
import io.ktor.client.plugins.websocket.webSocket
|
import io.ktor.client.plugins.websocket.webSocket
|
||||||
import io.ktor.websocket.DefaultWebSocketSession
|
import io.ktor.websocket.DefaultWebSocketSession
|
||||||
import io.ktor.websocket.send
|
import io.ktor.websocket.send
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
class WSClient(
|
class WSClient(
|
||||||
private val coroutineScope: CoroutineScope,
|
|
||||||
private val baseUrl: String,
|
private val baseUrl: String,
|
||||||
private val accessToken: String,
|
private val accessToken: String,
|
||||||
private val httpClient: HttpClient,
|
private val httpClient: HttpClient,
|
||||||
|
|
@ -23,34 +19,31 @@ class WSClient(
|
||||||
private var iteratingNumber = 0
|
private var iteratingNumber = 0
|
||||||
|
|
||||||
suspend fun connect() {
|
suspend fun connect() {
|
||||||
GlobalScope.launch {
|
httpClient.webSocket(wsUrl) {
|
||||||
httpClient.webSocket(wsUrl) {
|
session = this
|
||||||
session = this
|
while (true) {
|
||||||
while (true) {
|
try {
|
||||||
try {
|
val data = receiveDeserialized<WebSocketIncoming>()
|
||||||
val data = receiveDeserialized<WebSocketIncoming>()
|
when (data.type) {
|
||||||
when (data.type) {
|
"auth_required" -> {
|
||||||
"auth_required" -> {
|
println("WebSocket: Authentication required")
|
||||||
println("WebSocket: Authentication required")
|
sendSerialized(WSAuth("auth", accessToken))
|
||||||
sendSerialized(WSAuth("auth", accessToken))
|
}
|
||||||
}
|
|
||||||
|
"auth_ok" -> {
|
||||||
"auth_ok" -> {
|
println("WebSocket: Authentication OK")
|
||||||
println("WebSocket: Authentication OK")
|
val data1 = WSSubscribe(iteratingNumber)
|
||||||
val data1 = WSSubscribe(iteratingNumber)
|
val serialized = Json.encodeToString(data1)
|
||||||
val serialized = Json.encodeToString(data1)
|
sendSerialized(data1)
|
||||||
// send(serialized)
|
}
|
||||||
sendSerialized(data1)
|
|
||||||
}
|
else -> {
|
||||||
|
println("WebSocket: Misc data received: $data")
|
||||||
else -> {
|
|
||||||
println("WebSocket: Misc data received: $data")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
iteratingNumber++
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println("WS Exception: $e")
|
|
||||||
}
|
}
|
||||||
|
iteratingNumber++
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("WS Exception: $e")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
52
app/src/main/java/dev/coffeeco/homenative/data/WSIncoming.kt
Normal file
52
app/src/main/java/dev/coffeeco/homenative/data/WSIncoming.kt
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
|
@ -4,11 +4,6 @@ import kotlinx.serialization.EncodeDefault
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class PostServiceBody(
|
|
||||||
@SerialName("entity_id") val entityId: String
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
/**
|
/**
|
||||||
* Special since `id` field must be omitted in contrast to [WebSocketMessage.id]
|
* Special since `id` field must be omitted in contrast to [WebSocketMessage.id]
|
||||||
|
|
@ -22,11 +17,14 @@ data class WSAuth(
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
sealed class WebSocketMessage {
|
/**
|
||||||
|
* Generic format for outgoing messages
|
||||||
|
*/
|
||||||
|
sealed class WebSocketMessage(
|
||||||
@EncodeDefault
|
@EncodeDefault
|
||||||
@SerialName("type")
|
@SerialName("type")
|
||||||
abstract val type: String
|
val type: String,
|
||||||
|
) {
|
||||||
@SerialName("id")
|
@SerialName("id")
|
||||||
abstract val id: Int
|
abstract val id: Int
|
||||||
}
|
}
|
||||||
|
|
@ -35,13 +33,6 @@ sealed class WebSocketMessage {
|
||||||
data class WSSubscribe(
|
data class WSSubscribe(
|
||||||
@SerialName("id")
|
@SerialName("id")
|
||||||
override val id: Int,
|
override val id: Int,
|
||||||
// @EncodeDefault
|
|
||||||
// @SerialName("type")
|
|
||||||
// val type: String = "subscribe_events",
|
|
||||||
@SerialName("event_type")
|
@SerialName("event_type")
|
||||||
val eventType: String? = null,
|
val eventType: String? = null,
|
||||||
) : WebSocketMessage() {
|
) : WebSocketMessage(type = "subscribe_events")
|
||||||
@EncodeDefault
|
|
||||||
@SerialName("type")
|
|
||||||
override val type: String = "subscribe_events"
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue