package dev.asdf00.visionfive.hc.server import com.sun.net.httpserver.HttpExchange 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 = getAuthToken(exchange) LoginBehavior.isLoggedIn(token)?.let { uname -> // logged in exchange.sendReply( //@formatter:off 200, ContentType.HTML, """ VFHeatControl Landing Page

Welcome to HeatControl

You are logged in as ${uname}. To log out, click here.

The current recorded temperature is currently ${String.format("%.1f", GlobalDataStore.temperature)} C
Last updated ${GlobalDataStore.tempUpdated}
Current forecast for today: ${if (GlobalDataStore.isSunny) "sunny" else "cloudy"}

PID values

Temp:
P:
I:
D:

""".trimIndent().uft8() //@formatter:on ) } ?: run { // logged out exchange.sendReply( 200, ContentType.HTML, """ VFHeatControl Landing Page

Welcome to HeatControl

Please log in to view the current temperature


Login Form





""".trimIndent().uft8() ) } } // login logic path("login") { endpoint("in") handler@{ exchange -> if (exchange.requestMethod != "POST") { exchange.sendReply(400, ContentType.PLAIN, "wrong request method".uft8()) return@handler } val (userName, pwd) = exchange.requestBody.readAllBytes().utf8().split("&").map { it.split("=").let { if (it.size == 2) it[1] else "" } }.let { if (it.size == 2) Pair(it[0], it[1]) else Pair("", "") } LoginBehavior.login(userName, pwd)?.let { exchange.responseHeaders.add("Set-Cookie", "auth=${it}; Path=/; HttpOnly; SameSite=Strict") exchange.sendReply( 200, ContentType.HTML, "".uft8() ) } ?: run { exchange.sendReply( 400, ContentType.HTML, "".uft8() ) } } endpoint("out") { exchange -> val token = getAuthToken(exchange) if (LoginBehavior.logout(token)) { exchange.responseHeaders.add("Set-Cookie", "auth=; Path=/") exchange.sendReply( 200, ContentType.HTML, "".uft8() ) } else { exchange.sendReply( 400, ContentType.HTML, "".uft8() ) } } } // pid control endpoint("pid") handler@{ exchange -> if (LoginBehavior.isLoggedIn(getAuthToken(exchange)) == null) { // not logged in exchange.replyCat(401) return@handler } if (exchange.requestMethod == "POST") { // new values try { val data = JSONObject(exchange.requestBody.readAllBytes().utf8()).toMap() if (!data.containsKey("temp") || !data.containsKey("p") || !data.containsKey("i") || !data.containsKey("d")) { exchange.replyCat(400) } else { val temp = data["temp"].toString().toDouble() val p = data["p"].toString().toDouble() val i = data["i"].toString().toDouble() val d = data["d"].toString().toDouble() GlobalDataStore.targetTemp = temp GlobalDataStore.pval = p GlobalDataStore.ival = i GlobalDataStore.dval = d exchange.replyCat(200) } } catch (e: Exception) { exchange.replyCat(400) } } 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() ) } else { exchange.replyCat(400) } } } val tempServer = buildWebServer(8088) { // data collection endpoint("temp") handler@{ exchange -> exchange.requestURI.query.split("&").forEach { if (it.startsWith("t=")) { it.substring(2).toDoubleOrNull()?.let { GlobalDataStore.temperature = it GlobalDataStore.tempUpdated = Date() exchange.replyCat(200) } ?: run { exchange.replyCat(400) } return@handler } } exchange.replyCat(400) } } 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") while (!Thread.interrupted()) { println() print("Username to add: ") val userName = readln() print("Password for '$userName': ") 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 getAuthToken(exchange: HttpExchange) = exchange.requestHeaders["Cookie"] ?.let { if (it.size > 0) it[0] else null } ?.let { it.split("; ") } ?.filter { it.startsWith("auth=") } ?.firstOrNull() ?.let { it.split("=").let { if (it.size == 2) it[1] else "" } } ?: ""