From d17f4af1d6517a0e77575c98c75375b10d1e9749 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Mikul=C3=A1=C5=A1ek?= Date: Thu, 21 Mar 2024 16:41:43 +0100 Subject: [PATCH] feat: introduce http POST --- examples/java/build.gradle.kts | 2 +- examples/java/settings.gradle.kts | 2 +- .../loxone/example/BlockingHttpLoxoneClient.java | 5 ----- .../loxone/example/LoxoneClientExample.java | 15 ++++----------- src/commonMain/kotlin/LoxoneClient.kt | 16 +++++++++++++++- .../kotlin/ktor/KtorHttpLoxoneClient.kt | 11 +++++++++++ .../kotlin/ktor/HttpLoxoneClientTest.kt | 15 ++++++++++++++- 7 files changed, 46 insertions(+), 20 deletions(-) diff --git a/examples/java/build.gradle.kts b/examples/java/build.gradle.kts index 01fd92c..8c8cfde 100644 --- a/examples/java/build.gradle.kts +++ b/examples/java/build.gradle.kts @@ -8,7 +8,7 @@ repositories { } dependencies { - implementation("cz.smarteon.loxone:loxone-client-kotlin:0.1.0") + implementation("cz.smarteon.loxone:loxone-client-kotlin") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.1") } diff --git a/examples/java/settings.gradle.kts b/examples/java/settings.gradle.kts index 6bad623..674a396 100644 --- a/examples/java/settings.gradle.kts +++ b/examples/java/settings.gradle.kts @@ -1,3 +1,3 @@ -//includeBuild("../..") +includeBuild("../..") rootProject.name = "loxone-client-kotlin-java-example" diff --git a/examples/java/src/main/java/cz/smarteon/loxone/example/BlockingHttpLoxoneClient.java b/examples/java/src/main/java/cz/smarteon/loxone/example/BlockingHttpLoxoneClient.java index 91e1510..ee25d20 100644 --- a/examples/java/src/main/java/cz/smarteon/loxone/example/BlockingHttpLoxoneClient.java +++ b/examples/java/src/main/java/cz/smarteon/loxone/example/BlockingHttpLoxoneClient.java @@ -3,14 +3,9 @@ import cz.smarteon.loxone.Command; import cz.smarteon.loxone.LoxoneClient; import cz.smarteon.loxone.LoxoneResponse; -import kotlin.coroutines.Continuation; import kotlin.coroutines.EmptyCoroutineContext; import kotlinx.coroutines.BuildersKt; -import kotlinx.coroutines.CoroutineScope; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.Objects; import static java.util.Objects.requireNonNull; diff --git a/examples/java/src/main/java/cz/smarteon/loxone/example/LoxoneClientExample.java b/examples/java/src/main/java/cz/smarteon/loxone/example/LoxoneClientExample.java index 482d59c..944ce7f 100644 --- a/examples/java/src/main/java/cz/smarteon/loxone/example/LoxoneClientExample.java +++ b/examples/java/src/main/java/cz/smarteon/loxone/example/LoxoneClientExample.java @@ -1,11 +1,9 @@ package cz.smarteon.loxone.example; +import cz.smarteon.loxone.LoxoneAuth; import cz.smarteon.loxone.LoxoneClient; -import cz.smarteon.loxone.LoxoneCredentials; import cz.smarteon.loxone.LoxoneEndpoint; -import cz.smarteon.loxone.LoxoneProfile; -import cz.smarteon.loxone.LoxoneTokenAuthenticator; -import cz.smarteon.loxone.ktor.HttpLoxoneClient; +import cz.smarteon.loxone.ktor.KtorHttpLoxoneClient; import cz.smarteon.loxone.message.ApiInfo; public class LoxoneClientExample { @@ -14,14 +12,9 @@ public static void main(String[] args) { System.out.println("Test"); final var endpoint = new LoxoneEndpoint(args[0], 443); - final LoxoneClient loxoneClient = new HttpLoxoneClient( + final LoxoneClient loxoneClient = new KtorHttpLoxoneClient( endpoint, - new LoxoneTokenAuthenticator( - new LoxoneProfile( - endpoint, - new LoxoneCredentials(args[1], args[2]) - ) - ) + new LoxoneAuth.Basic(args[1], args[2]) ); final var client = new BlockingHttpLoxoneClient(loxoneClient); diff --git a/src/commonMain/kotlin/LoxoneClient.kt b/src/commonMain/kotlin/LoxoneClient.kt index 3cc2b40..bd00f7b 100644 --- a/src/commonMain/kotlin/LoxoneClient.kt +++ b/src/commonMain/kotlin/LoxoneClient.kt @@ -13,7 +13,21 @@ interface LoxoneClient { suspend fun close() } -interface HttpLoxoneClient : LoxoneClient +/** + * Loxone client using HTTP for communication. + */ +interface HttpLoxoneClient : LoxoneClient { + /** + * Sends given [payload] to [command]'s endpoint using HTTP POST to Loxone and returns + * the response body as text. + * + * @param command Command to call. + * @param payload Payload to send. + * @return Response body as text. + */ + suspend fun postRaw(command: String, payload: ByteArray): String +} + interface WebsocketLoxoneClient : LoxoneClient suspend inline fun LoxoneClient.callForMsg(command: LoxoneMsgCommand): VAL { diff --git a/src/commonMain/kotlin/ktor/KtorHttpLoxoneClient.kt b/src/commonMain/kotlin/ktor/KtorHttpLoxoneClient.kt index e4ce2ba..1949f4a 100644 --- a/src/commonMain/kotlin/ktor/KtorHttpLoxoneClient.kt +++ b/src/commonMain/kotlin/ktor/KtorHttpLoxoneClient.kt @@ -13,6 +13,7 @@ import io.ktor.client.engine.* import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.plugins.logging.* import io.ktor.client.request.* +import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.serialization.kotlinx.json.* import kotlin.jvm.JvmOverloads @@ -68,6 +69,16 @@ class KtorHttpLoxoneClient internal constructor( }.body() } + override suspend fun postRaw(command: String, payload: ByteArray): String { + authentication.tokenAuthenticator?.ensureAuthenticated(this) + return httpClient.post { + commandRequest { + appendPathSegments(command) + } + setBody(payload) + }.bodyAsText() + } + @Suppress("TooGenericExceptionCaught") override suspend fun close() { try { diff --git a/src/commonTest/kotlin/ktor/HttpLoxoneClientTest.kt b/src/commonTest/kotlin/ktor/HttpLoxoneClientTest.kt index 818378a..6360239 100644 --- a/src/commonTest/kotlin/ktor/HttpLoxoneClientTest.kt +++ b/src/commonTest/kotlin/ktor/HttpLoxoneClientTest.kt @@ -20,6 +20,7 @@ import io.kotest.matchers.shouldBe import io.ktor.client.engine.mock.* import io.ktor.client.plugins.* import io.ktor.http.* +import io.ktor.utils.io.core.* class HttpLoxoneClientTest : WordSpec({ @@ -52,7 +53,12 @@ class HttpLoxoneClientTest : WordSpec({ path.startsWith("/jdev/sys/getjwt") -> { val validUntil = TimeUtils.currentLoxoneSeconds().plus(60) - respondJson(okMsg(path, token(validUntil))) + respondJson(okMsg(path.substring(1), token(validUntil))) + } + + request.method == HttpMethod.Post && path.startsWith("/dev/fsput") -> { + val payloadSize = request.body.toByteArray().size + respondJson(okMsg(path.substring(1), payloadSize.toString())) } else -> respondHtmlError(HttpStatusCode.NotFound) @@ -101,6 +107,13 @@ class HttpLoxoneClientTest : WordSpec({ response.code shouldBe "200" client.close() } + + "call post" { + val client = KtorHttpLoxoneClient(endpoint, LoxoneAuth.Basic(profile), mockEngine) + val response = client.postRaw("dev/fsput/test", "test".toByteArray()) + response shouldBe okMsg("dev/fsput/test", "4") + client.close() + } } })