From 8096c57952340a9d2c4b43245ac187efd0bb5148 Mon Sep 17 00:00:00 2001 From: 00asdf Date: Fri, 25 Apr 2025 22:13:46 +0200 Subject: [PATCH] weather forecast --- .../asdf00/visionfive/hc/GlobalDataStore.kt | 3 + .../visionfive/hc/control/PidController.kt | 4 + .../visionfive/hc/server/LoginBehavior.kt | 8 +- .../dev/asdf00/visionfive/hc/server/Main.kt | 99 +++++++++++++++++-- 4 files changed, 101 insertions(+), 13 deletions(-) create mode 100644 src/main/kotlin/dev/asdf00/visionfive/hc/control/PidController.kt diff --git a/src/main/kotlin/dev/asdf00/visionfive/hc/GlobalDataStore.kt b/src/main/kotlin/dev/asdf00/visionfive/hc/GlobalDataStore.kt index 2e25926..01a94a8 100644 --- a/src/main/kotlin/dev/asdf00/visionfive/hc/GlobalDataStore.kt +++ b/src/main/kotlin/dev/asdf00/visionfive/hc/GlobalDataStore.kt @@ -6,9 +6,12 @@ object GlobalDataStore { @Volatile var temperature = 0.0 @Volatile var tempUpdated = Date() @Volatile var isSunny = true + @Volatile var resetTime = Date() @Volatile var targetTemp = 22.5 @Volatile var pval = 0.0 @Volatile var ival = 1.0 @Volatile var dval = 2.0 + + const val API = "https://api.open-meteo.com/v1/forecast?latitude=47.6649&longitude=14.3401&daily=sunshine_duration,daylight_duration&hourly=temperature_2m&timezone=Europe%2FBerlin&forecast_days=1&format=json&timeformat=unixtime" } \ No newline at end of file diff --git a/src/main/kotlin/dev/asdf00/visionfive/hc/control/PidController.kt b/src/main/kotlin/dev/asdf00/visionfive/hc/control/PidController.kt new file mode 100644 index 0000000..7ce2188 --- /dev/null +++ b/src/main/kotlin/dev/asdf00/visionfive/hc/control/PidController.kt @@ -0,0 +1,4 @@ +package dev.asdf00.visionfive.hc.control + +class PidController { +} \ No newline at end of file diff --git a/src/main/kotlin/dev/asdf00/visionfive/hc/server/LoginBehavior.kt b/src/main/kotlin/dev/asdf00/visionfive/hc/server/LoginBehavior.kt index 464cb76..93ffe1a 100644 --- a/src/main/kotlin/dev/asdf00/visionfive/hc/server/LoginBehavior.kt +++ b/src/main/kotlin/dev/asdf00/visionfive/hc/server/LoginBehavior.kt @@ -15,7 +15,11 @@ object LoginBehavior { private val users = loadFromFile() operator fun get(userName: String) = users[userName] - operator fun set(userName: String, password: String) { + operator fun set(userName: String, password: String?) { + if (password == null) { + users.remove(userName) == null + return + } val salt = getRandomBase64(16) users[userName] = PwdHash(hashPassword(password, salt), salt) Files.writeString(FILE_PATH, users.map { @@ -36,8 +40,6 @@ object LoginBehavior { return mutableMapOf() } } - - fun remove(userName: String) = users.remove(userName) == null } private val loggedInUsers = mutableMapOf() diff --git a/src/main/kotlin/dev/asdf00/visionfive/hc/server/Main.kt b/src/main/kotlin/dev/asdf00/visionfive/hc/server/Main.kt index ecea7c3..f91f77b 100644 --- a/src/main/kotlin/dev/asdf00/visionfive/hc/server/Main.kt +++ b/src/main/kotlin/dev/asdf00/visionfive/hc/server/Main.kt @@ -1,19 +1,29 @@ package dev.asdf00.visionfive.hc.server import com.sun.net.httpserver.HttpExchange -import dev.asdf00.visionfive.hc.GlobalDataStore -import java.nio.charset.StandardCharsets -import java.util.* +import dev.asdf00.visionfive.hc.GlobalDataStore + +import org.json.JSONArray +import org.json.JSONException import org.json.JSONObject +import java.math.BigDecimal +import java.net.URI +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse +import java.nio.charset.StandardCharsets +import java.time.Duration +import java.util.* + fun main() { // out-facing web page val server = buildWebServer(8030) { // main landing page endpoint("") { exchange -> exchange.responseHeaders.add("Cache-Control", "no-store") - val token = getLoginToken(exchange) + val token = getAuthToken(exchange) LoginBehavior.isLoggedIn(token)?.let { uname -> // logged in exchange.sendReply( @@ -113,7 +123,6 @@ fun main() { } LoginBehavior.login(userName, pwd)?.let { exchange.responseHeaders.add("Set-Cookie", "auth=${it}; Path=/; HttpOnly; SameSite=Strict") - exchange.responseHeaders.add("Set-Cookie", "test=2; Path=/; HttpOnly; SameSite=Strict") exchange.sendReply( 200, ContentType.HTML, @@ -129,7 +138,7 @@ fun main() { } endpoint("out") { exchange -> - val token = getLoginToken(exchange) + val token = getAuthToken(exchange) if (LoginBehavior.logout(token)) { exchange.responseHeaders.add("Set-Cookie", "auth=; Path=/") exchange.sendReply( @@ -149,7 +158,7 @@ fun main() { // pid control endpoint("pid") handler@{ exchange -> - if (LoginBehavior.isLoggedIn(getLoginToken(exchange)) == null) { + if (LoginBehavior.isLoggedIn(getAuthToken(exchange)) == null) { // not logged in exchange.replyCat(401) return@handler @@ -158,7 +167,7 @@ fun main() { // new values try { val data = JSONObject(exchange.requestBody.readAllBytes().utf8()).toMap() - if (!data.containsKey("temp") || !data.containsKey("p") || !data.containsKey("i") || ! data.containsKey("d")) { + if (!data.containsKey("temp") || !data.containsKey("p") || !data.containsKey("i") || !data.containsKey("d")) { exchange.replyCat(400) } else { val temp = data["temp"].toString().toDouble() @@ -176,7 +185,11 @@ fun main() { } } else if (exchange.requestMethod == "GET") { // get current values - exchange.sendReply(200, ContentType.JSON, "{\"temp\": ${GlobalDataStore.targetTemp}, \"p\": ${GlobalDataStore.pval}, \"i\": ${GlobalDataStore.ival}, \"d\": ${GlobalDataStore.dval}}".uft8()) + exchange.sendReply( + 200, + ContentType.JSON, + "{\"temp\": ${GlobalDataStore.targetTemp}, \"p\": ${GlobalDataStore.pval}, \"i\": ${GlobalDataStore.ival}, \"d\": ${GlobalDataStore.dval}}".uft8() + ) } else { exchange.replyCat(400) } @@ -202,6 +215,71 @@ fun main() { } } + val forecastQuery = Thread { + while (!Thread.interrupted()) { + var success = false + val request = HttpRequest.newBuilder(URI.create(GlobalDataStore.API)).GET().build() + HttpClient.newHttpClient() + .sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .thenApply { response -> + if (response.statusCode() == 200 && response.headers().firstValue("Content-Type") + .map { it.contains(ContentType.JSON.text) }.orElse(false) + ) { + try { + val data = JSONObject(response.body()) + var maxTemp = Double.NEGATIVE_INFINITY + var maxTempTime = -1 + ((data["hourly"] as? JSONObject)?.get("temperature_2m") as? JSONArray)?.forEachIndexed { h, t -> + (t as? BigDecimal)?.let { + val temp = it.toDouble() + if (maxTemp < temp) { + maxTemp = temp + maxTempTime = h + } + } + } + if (maxTempTime > 0) { + val daylight = ((data["daily"] as? JSONObject)?.get("daylight_duration") as? JSONArray)?.let { + if (it.length() == 1) + (it[0] as? BigDecimal)?.let { it.toDouble() } + else + null + } ?: 0.0 + val sun = ((data["daily"] as? JSONObject)?.get("sunshine_duration") as? JSONArray)?.let { + if (it.length() == 1) + (it[0] as? BigDecimal)?.let { it.toDouble() } + else + null + } ?: 0.0 + + GlobalDataStore.resetTime = Calendar.getInstance().also { + it.set(Calendar.HOUR_OF_DAY, maxTempTime) + it.set(Calendar.MINUTE, 30) + it.set(Calendar.SECOND, 0) + }.time + GlobalDataStore.isSunny = sun > daylight / 2 + success = true + } else { + println("error reading weather API response") + } + } catch (e: JSONException) { + println("error parsing weather API response") + } + } else { + println("error querying weather API response") + } + } + .join() + + if (success) { + Thread.sleep(Duration.ofHours(12)) + } else { + Thread.sleep(Duration.ofMinutes(30)) + } + } + } + + forecastQuery.start() server.start() tempServer.start() println("HttpServer is running, press CTRL+C to exit") @@ -213,13 +291,14 @@ fun main() { val pwd = readln() LoginBehavior.Users[userName] = pwd } + forecastQuery.interrupt() } fun String.uft8() = toByteArray(StandardCharsets.UTF_8) fun ByteArray.utf8() = String(this, StandardCharsets.UTF_8) -private fun getLoginToken(exchange: HttpExchange) = +private fun getAuthToken(exchange: HttpExchange) = exchange.requestHeaders["Cookie"] ?.let { if (it.size > 0) it[0] else null } ?.let { it.split("; ") }