Monochrome, print-ready documentation. Emphasis on exact formulas, unit handling, payloads, and drop-in code.
Compute two derived climate metrics for the tent from air temperature and relative humidity:
Implementation uses Tetens for saturation vapor pressure and Magnus for dew point. Constants match the dashboard implementation.
| Key | Type | Units | Notes |
|---|---|---|---|
| air_temp_f | float | °F | From your DHTxx/SHTxx/BME280 pipeline. Convert to °C for math. |
| rh_pct | float | % | 0–100. Clamp to [1, 100] to avoid log(0) in Magnus. |
| leaf_temp_c | float (optional) | °C | If you have IR leaf temp, use it for VPD saturation term. Else use air temp. |
Use for saturation vapor pressure at temperature TC (°C):
// Saturation vapor pressure (kPa)
e_s(Tc) = 0.6108 * exp( (17.27 * Tc) / (Tc + 237.3) )
// Actual vapor pressure (kPa)
e_a = e_s(Tc_air) * (RH / 100.0)
// Vapor Pressure Deficit (kPa)
// Use Tc_leaf if available, else Tc_air
VPD = max(0, e_s(Tc_for_sat) - e_a)
0.6108 and 17.27/237.3 are used in the dashboard. Keep these constants consistent across server and browser.
Dew point Td (°C) from air temp and RH:
a = 17.62
b = 243.12 // °C
gamma = ln(RH/100) + (a * Tc_air) / (b + Tc_air)
Td_C = (b * gamma) / (a - gamma)
Td_F = Td_C * 9/5 + 32
Clamp RH to [1, 100] before computing ln(RH/100).
| Stage | VPD Target (kPa) | Notes |
|---|---|---|
| Seedling/Clones | 0.6–0.8 | Higher humidity to reduce stress. |
| Vegetative | 0.8–1.2 | Balanced growth and transpiration. |
| Flower/Fruiting | 1.2–1.6 | Lower humidity to limit mold risk. |
Assume your server exposes the latest air temp and RH, and accepts derived metrics:
| GET latest env | GET http://<host>:5001/api/latest |
|---|---|
| POST derived | POST http://<host>:5001/add_data (single reading per request) |
| Content-Type | application/json |
| Interval | Every 60 s (or on new sensor sample) |
| Timeout | 5 s |
/api/latest){
"temp": 77.9, // °F
"humidity": 58.2, // % RH
"light": 123 // lux (example)
}
/add_data){
"sensor_type": "vpd_kpa", // or "dew_point_f"
"reading": 1.12, // numeric value only
"plant_id": null // optional
}
// Post one reading per request.
// Example (dew point): {"sensor_type":"dew_point_f","reading":61.7}
Return HTTP 200/201 on success. Log non-2xx with input values.
# Pure functions with explicit units and clamps.
# Matches the dashboard constants (Tetens + Magnus).
import math
from typing import Tuple
A_TETENS = 17.27
B_TETENS = 237.3 # °C
K0 = 0.6108 # kPa
A_MAGNUS = 17.62
B_MAGNUS = 243.12 # °C
def f_to_c(tf: float) -> float:
return (tf - 32.0) * (5.0/9.0)
def c_to_f(tc: float) -> float:
return tc * (9.0/5.0) + 32.0
def tetens_es_kpa(tc: float) -> float:
# Saturation vapor pressure (kPa) at Tc (°C)
return K0 * math.exp((A_TETENS * tc) / (B_TETENS + tc))
def dew_point_f_from_air(tf_air: float, rh_pct: float) -> float:
# Clamp RH to [1,100] to avoid log(0) and nonsense
rh = max(1.0, min(100.0, rh_pct))
tc = f_to_c(tf_air)
gamma = math.log(rh / 100.0) + (A_MAGNUS * tc) / (B_MAGNUS + tc)
td_c = (B_MAGNUS * gamma) / (A_MAGNUS - gamma)
td_f = c_to_f(td_c)
return td_f
def vpd_kpa_from_air(tf_air: float, rh_pct: float, tf_leaf: float | None = None) -> float:
# Use leaf temperature for saturation term if available; else air temp.
rh = max(0.0, min(100.0, rh_pct))
tc_air = f_to_c(tf_air)
tc_sat = f_to_c(tf_leaf) if tf_leaf is not None else tc_air
es = tetens_es_kpa(tc_sat) # saturation at leaf/air
ea = tetens_es_kpa(tc_air) * (rh/100.0) # actual vapor pressure from air
vpd = max(0.0, es - ea) # never negative
return vpd
# Example usage with prints for debug:
if __name__ == "__main__":
tf = 77.9
rh = 58.2
print(f"[DEBUG] air_temp_f={tf} rh_pct={rh}")
dp = dew_point_f_from_air(tf, rh)
vpd = vpd_kpa_from_air(tf, rh) # no leaf temp
print(f"[DEBUG] dew_point_f={dp:.2f} vpd_kpa={vpd:.3f}")
<script>
// Minimal, no dependencies. Mirrors the dashboard implementation.
const A_T = 17.27, B_T = 237.3, K0 = 0.6108; // Tetens (kPa)
const A_M = 17.62, B_M = 243.12; // Magnus (dew point)
function fToC(tf){ return (tf - 32) * (5/9); }
function cToF(tc){ return tc * (9/5) + 32; }
function tetensKPa(tc){ return K0 * Math.exp((A_T * tc) / (B_T + tc)); }
function dewPointF(airTempF, rhPct){
const rh = Math.min(100, Math.max(1, rhPct));
const Tc = fToC(airTempF);
const gamma = Math.log(rh/100) + (A_M * Tc) / (B_M + Tc);
const TdC = (B_M * gamma) / (A_M - gamma);
const TdF = cToF(TdC);
console.log("[dp] Tc=%s rh=%s TdF=%s", Tc.toFixed(2), rh.toFixed(1), TdF.toFixed(2));
return TdF;
}
function vpdKPa(airTempF, rhPct, leafTempF=null){
const rh = Math.min(100, Math.max(0, rhPct));
const Tc_air = fToC(airTempF);
const Tc_sat = leafTempF==null ? Tc_air : fToC(leafTempF);
const es = tetensKPa(Tc_sat);
const ea = tetensKPa(Tc_air) * (rh/100);
const vpd = Math.max(0, es - ea);
console.log("[vpd] es=%s ea=%s vpd=%s", es.toFixed(4), ea.toFixed(4), vpd.toFixed(4));
return vpd;
}
</script>
Drop this into your dashboard page. It calls /api/latest and renders live VPD and dew point.
<div id="vpdbox" class="card">
<strong>Live Air:</strong> <span id="air">--</span> °F,
<strong>RH:</strong> <span id="rh">--</span> %,
<strong>VPD:</strong> <span id="vpd">--</span> kPa,
<strong>Dew Pt:</strong> <span id="dp">--</span> °F
</div>
<script>
async function refreshVPD(){
try{
const r = await fetch("api/latest.json", { cache:"no-store" });
if(!r.ok) throw new Error("HTTP " + r.status);
const j = await r.json();
const tf = Number(j.temp);
const rh = Number(j.humidity);
const v = vpdKPa(tf, rh);
const dpf = dewPointF(tf, rh);
document.getElementById("air").textContent = tf.toFixed(1);
document.getElementById("rh").textContent = rh.toFixed(1);
document.getElementById("vpd").textContent = v.toFixed(2);
document.getElementById("dp").textContent = dpf.toFixed(1);
console.log("[widget] tf=%s rh=%s vpd=%s dp=%s", tf, rh, v, dpf);
}catch(err){
console.log("[widget] refresh error:", err);
}
}
refreshVPD();
setInterval(refreshVPD, 60000);
</script>
| Item | Value |
|---|---|
| Tetens constant (kPa) | 0.6108 |
| Tetens a | 17.27 |
| Tetens b (°C) | 237.3 |
| Magnus a | 17.62 |
| Magnus b (°C) | 243.12 |
| F→C | (F − 32) × 5/9 |
| C→F | C × 9/5 + 32 |
This document is intentionally black-and-white for clean printing and photocopying.