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 "" } }
?: ""