This commit is contained in:
Laurenz 2024-09-03 13:17:05 +02:00
parent fc1a8e0367
commit 89ea5df36e
Signed by: C0ffeeCode
SSH key fingerprint: SHA256:jnEltBNftC3wUZESLSMvM9zVPOkkevGRzqqoW2k2ORI
7 changed files with 110 additions and 124 deletions

View file

@ -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,13 +59,15 @@ class MainActivity : ComponentActivity() {
@Composable @Composable
private fun AppContainer() { private fun AppContainer() {
val ctx = LocalContext.current val ctx = LocalContext.current
LaunchedEffect(Unit) {
if (homeClient == null) { if (homeClient == null) {
val cf = obtainClient(ctx) val cf = obtainClient(ctx)
runBlocking { lifecycleScope.launch(Dispatchers.IO) {
homeClient = cf.first() homeClient = cf.first()
homeClient!!.wsClient.connect() homeClient!!.wsClient.connect()
} }
} }
}
val onLoginSubmit: suspend (String, String) -> Unit = { url: String, token: String -> val onLoginSubmit: suspend (String, String) -> Unit = { url: String, token: String ->
setInstance(ctx, url, token, refreshToken = null) setInstance(ctx, url, token, refreshToken = null)

View file

@ -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/")

View file

@ -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
)

View file

@ -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"
}

View file

@ -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,7 +19,6 @@ 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) {
@ -39,7 +34,6 @@ class WSClient(
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)
// send(serialized)
sendSerialized(data1) sendSerialized(data1)
} }
@ -54,7 +48,6 @@ class WSClient(
} }
} }
} }
}
suspend fun send_dunnschiss() { suspend fun send_dunnschiss() {
while (session == null) { while (session == null) {

View 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"
}

View file

@ -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"
}