Initial Commit
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
use axum::{extract::State, Json};
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
|
||||
use super::{AppError, AppState};
|
||||
|
||||
/// Find the 7timer dataseries slot closest to tonight's dusk UTC.
|
||||
/// Falls back to slot[0] (now) if dusk is unavailable.
|
||||
fn find_tonight_slot(dataseries: &[serde_json::Value], init_str: &str, dusk_utc: Option<&str>) -> Option<serde_json::Value> {
|
||||
let dusk_utc = dusk_utc?;
|
||||
let dusk_dt = chrono::DateTime::parse_from_rfc3339(dusk_utc).ok()?;
|
||||
let dusk_epoch = dusk_dt.timestamp();
|
||||
|
||||
// 7timer init format: "2026040812" → 2026-04-08 12:00 UTC
|
||||
let init_dt = NaiveDateTime::parse_from_str(init_str, "%Y%m%d%H")
|
||||
.ok()
|
||||
.map(|dt| dt.and_utc().timestamp())?;
|
||||
|
||||
let mut best: Option<&serde_json::Value> = None;
|
||||
let mut best_diff = i64::MAX;
|
||||
for slot in dataseries {
|
||||
let tp = slot.get("timepoint")?.as_i64()?;
|
||||
let slot_epoch = init_dt + tp * 3600;
|
||||
let diff = (slot_epoch - dusk_epoch).abs();
|
||||
if diff < best_diff {
|
||||
best_diff = diff;
|
||||
best = Some(slot);
|
||||
}
|
||||
}
|
||||
best.cloned()
|
||||
}
|
||||
|
||||
pub async fn get_weather(
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<serde_json::Value>, AppError> {
|
||||
let row = sqlx::query("SELECT * FROM weather_cache WHERE id = 1")
|
||||
.fetch_optional(&state.pool)
|
||||
.await?;
|
||||
|
||||
match row {
|
||||
Some(r) => {
|
||||
use sqlx::Row;
|
||||
let seventimer_json: Option<String> = r.try_get("seventimer_json").unwrap_or_default();
|
||||
|
||||
let parsed_7t = seventimer_json.as_deref()
|
||||
.and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok());
|
||||
|
||||
let dataseries = parsed_7t.as_ref()
|
||||
.and_then(|v| v.get("dataseries"))
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|a| a.as_slice())
|
||||
.unwrap_or(&[]);
|
||||
|
||||
let init_str = parsed_7t.as_ref()
|
||||
.and_then(|v| v.get("init"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
// Load tonight's dusk to pick the relevant forecast slot
|
||||
let dusk_utc: Option<String> = sqlx::query_scalar(
|
||||
"SELECT astro_dusk_utc FROM tonight WHERE id = 1"
|
||||
)
|
||||
.fetch_optional(&state.pool)
|
||||
.await
|
||||
.unwrap_or(None);
|
||||
|
||||
// Try to get the slot nearest to tonight's dusk; fall back to first slot
|
||||
let tonight_slot = find_tonight_slot(dataseries, init_str, dusk_utc.as_deref())
|
||||
.or_else(|| dataseries.first().cloned());
|
||||
|
||||
// Also keep current slot (slot[0]) for actual current conditions
|
||||
let current_slot = dataseries.first().cloned();
|
||||
|
||||
let dew_alert = {
|
||||
let temp = r.try_get::<Option<f64>, _>("temp_c").unwrap_or_default().unwrap_or(20.0);
|
||||
let dew = r.try_get::<Option<f64>, _>("dew_point_c").unwrap_or_default().unwrap_or(10.0);
|
||||
let margin = temp - dew;
|
||||
if margin < 2.0 { Some("critical") }
|
||||
else if margin < 4.0 { Some("warning") }
|
||||
else { None }
|
||||
};
|
||||
|
||||
let go_nogo_str = r.try_get::<Option<String>, _>("go_nogo").unwrap_or_default();
|
||||
|
||||
// Build go_nogo_reasons from tonight's slot
|
||||
let slot_for_reasons = tonight_slot.as_ref().or(current_slot.as_ref());
|
||||
let go_nogo_reasons = slot_for_reasons.map(|slot| {
|
||||
let mut reasons = Vec::<String>::new();
|
||||
if let Some(cc) = slot.get("cloudcover").and_then(|v| v.as_i64()) {
|
||||
if cc > 4 { reasons.push(format!("Cloud cover {}/9", cc)); }
|
||||
}
|
||||
if let Some(see) = slot.get("seeing").and_then(|v| v.as_i64()) {
|
||||
if see > 5 { reasons.push(format!("Poor seeing ({}/8)", see)); }
|
||||
}
|
||||
if let Some(tr) = slot.get("transparency").and_then(|v| v.as_i64()) {
|
||||
if tr > 5 { reasons.push(format!("Low transparency ({}/8)", tr)); }
|
||||
}
|
||||
if let Some(li) = slot.get("lifted_index").and_then(|v| v.as_i64()) {
|
||||
if li < -2 { reasons.push(format!("Unstable atmosphere (LI {})", li)); }
|
||||
}
|
||||
reasons
|
||||
}).unwrap_or_default();
|
||||
|
||||
// Recompute go/nogo from tonight's slot
|
||||
let tonight_go_nogo = tonight_slot.as_ref().map(|slot| {
|
||||
let cc = slot.get("cloudcover").and_then(|v| v.as_i64()).unwrap_or(5);
|
||||
let see = slot.get("seeing").and_then(|v| v.as_i64()).unwrap_or(5);
|
||||
let tr = slot.get("transparency").and_then(|v| v.as_i64()).unwrap_or(5);
|
||||
if cc <= 2 && see <= 3 && tr <= 3 { "go" }
|
||||
else if cc <= 4 && see <= 5 { "marginal" }
|
||||
else { "nogo" }
|
||||
}).or(go_nogo_str.as_deref());
|
||||
|
||||
let s = tonight_slot.as_ref();
|
||||
Ok(Json(serde_json::json!({
|
||||
"dew_point_c": r.try_get::<Option<f64>, _>("dew_point_c").unwrap_or_default(),
|
||||
"temp_c": r.try_get::<Option<f64>, _>("temp_c").unwrap_or_default(),
|
||||
"humidity_pct": r.try_get::<Option<f64>, _>("humidity_pct").unwrap_or_default(),
|
||||
"go_nogo": tonight_go_nogo,
|
||||
"go_nogo_reasons": go_nogo_reasons,
|
||||
"fetched_at": r.try_get::<Option<i64>, _>("fetched_at").unwrap_or_default(),
|
||||
"dew_alert": dew_alert,
|
||||
// Tonight's forecast slot fields
|
||||
"cloudcover": s.and_then(|s| s.get("cloudcover")).and_then(|v| v.as_i64()),
|
||||
"seeing": s.and_then(|s| s.get("seeing")).and_then(|v| v.as_i64()),
|
||||
"transparency": s.and_then(|s| s.get("transparency")).and_then(|v| v.as_i64()),
|
||||
"lifted_index": s.and_then(|s| s.get("lifted_index")).and_then(|v| v.as_i64()),
|
||||
"wind10m": s.and_then(|s| s.get("wind10m")).cloned(),
|
||||
"rh2m": s.and_then(|s| s.get("rh2m")).and_then(|v| v.as_i64()),
|
||||
})))
|
||||
}
|
||||
None => Ok(Json(serde_json::json!({ "go_nogo": null, "fetched_at": null }))),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_forecast(
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<serde_json::Value>, AppError> {
|
||||
let row = sqlx::query("SELECT seventimer_json FROM weather_cache WHERE id = 1")
|
||||
.fetch_optional(&state.pool)
|
||||
.await?;
|
||||
|
||||
let forecast = row
|
||||
.and_then(|r| {
|
||||
use sqlx::Row;
|
||||
r.try_get::<Option<String>, _>("seventimer_json").ok().flatten()
|
||||
})
|
||||
.and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
|
||||
.unwrap_or(serde_json::json!({}));
|
||||
|
||||
Ok(Json(forecast))
|
||||
}
|
||||
Reference in New Issue
Block a user