This commit is contained in:
Mateusz Gruszczyński
2026-08-24 15:20:29 +02:00
parent 4f70e36e89
commit 83f744e2cb
19 changed files with 744 additions and 176 deletions
+53 -1
View File
@@ -9,7 +9,7 @@ use axum::{
Json, Router,
};
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use chrono::{Duration as ChronoDuration, Utc};
use chrono::{Duration as ChronoDuration, NaiveTime, Utc};
use futures_util::StreamExt;
use rand::{rngs::OsRng, RngCore};
use serde::Deserialize;
@@ -59,6 +59,7 @@ pub fn router(state: AppState) -> Router {
.route("/api/history", get(history))
.route("/api/control-plan", get(control_plan))
.route("/api/events", get(events))
.route("/api/events/retention", get(get_event_retention).put(update_event_retention))
.route("/api/settings", get(get_settings).put(update_settings))
.route("/api/settings/export", get(export_settings))
.route("/api/settings/import", post(import_settings))
@@ -1060,6 +1061,27 @@ async fn events(State(state): State<AppState>, Query(query): Query<EventsQuery>)
Ok(Json(json!({"events": state.db.list_events(query.limit.unwrap_or(100))?})))
}
#[derive(Debug, Deserialize)]
struct EventRetentionInput { days: u32 }
async fn get_event_retention(State(state): State<AppState>) -> Json<Value> {
let days = state.settings.read().await.event_log_retention_days;
Json(json!({"days": days}))
}
async fn update_event_retention(State(state): State<AppState>, Json(input): Json<EventRetentionInput>) -> Result<Json<Value>, AppError> {
let mut settings = state.settings.write().await;
settings.event_log_retention_days = input.days.clamp(1, 3650);
state.db.save_runtime_settings(&settings)?;
let days = settings.event_log_retention_days;
drop(settings);
let removed = state.db.prune_events(days as i64)?;
state.log("info", "events.retention_updated", "Event log retention updated", json!({"days": days, "removed": removed}));
let public = { let settings = state.settings.read().await; public_settings(&*settings) };
state.broadcast("settings.updated", public);
Ok(Json(json!({"days": days, "removed": removed})))
}
async fn get_settings(State(state): State<AppState>) -> Json<Value> {
let settings = state.settings.read().await;
Json(public_settings(&*settings))
@@ -1080,6 +1102,9 @@ async fn update_settings(State(state): State<AppState>, Json(mut input): Json<Ru
if input.controller_id.trim().is_empty() { input.controller_id = old.controller_id; }
if input.home_assistant.token.trim().is_empty() { input.home_assistant.token = old.home_assistant.token; }
input.history_retention_days = input.history_retention_days.clamp(1, 3650);
input.event_log_retention_days = input.event_log_retention_days.clamp(1, 3650);
normalize_sensor_aliases(&mut input);
validate_night_mode(&mut input)?;
input.influxdb.history_threshold_days = input.influxdb.history_threshold_days.clamp(1, 3650);
if input.influxdb.token.trim().is_empty() { input.influxdb.token = old.influxdb.token; }
if input.influxdb.password.trim().is_empty() { input.influxdb.password = old.influxdb.password; }
@@ -1096,6 +1121,27 @@ async fn update_settings(State(state): State<AppState>, Json(mut input): Json<Ru
Ok(Json(public_settings(&input)))
}
fn normalize_sensor_aliases(settings: &mut RuntimeSettings) {
settings.home_assistant.sensor_aliases = settings.home_assistant.sensor_aliases
.iter()
.filter_map(|(entity, alias)| {
let entity = entity.trim();
let alias = alias.trim();
if entity.is_empty() || alias.is_empty() { return None; }
Some((entity.chars().take(160).collect::<String>(), alias.chars().take(80).collect::<String>()))
})
.collect();
}
fn validate_night_mode(settings: &mut RuntimeSettings) -> Result<(), AppError> {
NaiveTime::parse_from_str(&settings.night_mode.start_time, "%H:%M")
.map_err(|_| AppError::BadRequest("night mode start time must use HH:MM".into()))?;
NaiveTime::parse_from_str(&settings.night_mode.end_time, "%H:%M")
.map_err(|_| AppError::BadRequest("night mode end time must use HH:MM".into()))?;
settings.night_mode.max_fan_speed = settings.night_mode.max_fan_speed.clamp(1, 5);
Ok(())
}
async fn export_settings(State(state): State<AppState>) -> Result<Json<ConfigurationExport>, AppError> {
let settings = state.settings.read().await.clone();
Ok(Json(state.db.export_configuration(settings)?))
@@ -1124,6 +1170,9 @@ fn validate_configuration_export(export: &ConfigurationExport) -> Result<(), App
async fn import_settings(State(state): State<AppState>, Json(mut export): Json<ConfigurationExport>) -> Result<Json<Value>, AppError> {
validate_configuration_export(&export)?;
export.settings.history_retention_days = export.settings.history_retention_days.clamp(1, 3650);
export.settings.event_log_retention_days = export.settings.event_log_retention_days.clamp(1, 3650);
normalize_sensor_aliases(&mut export.settings);
validate_night_mode(&mut export.settings)?;
export.settings.influxdb.history_threshold_days = export.settings.influxdb.history_threshold_days.clamp(1, 3650);
state.db.replace_configuration(&export)?;
state.debug_gree_frames.store(export.settings.debug.gree_frames, Ordering::Relaxed);
@@ -1216,8 +1265,10 @@ fn public_settings(settings: &RuntimeSettings) -> Value {
"outdoor_assist_enabled": settings.outdoor_assist_enabled,
"history_retention_days": settings.history_retention_days,
"history_compaction_enabled": settings.history_compaction_enabled,
"event_log_retention_days": settings.event_log_retention_days,
"suppress_device_beep": settings.suppress_device_beep,
"debug": settings.debug,
"night_mode": settings.night_mode,
"influxdb": {
"enabled": settings.influxdb.enabled,
"version": settings.influxdb.version,
@@ -1239,6 +1290,7 @@ fn public_settings(settings: &RuntimeSettings) -> Value {
"default_entity_id": settings.home_assistant.default_entity_id,
"outdoor_entity_id": settings.home_assistant.outdoor_entity_id,
"allow_invalid_tls": settings.home_assistant.allow_invalid_tls,
"sensor_aliases": settings.home_assistant.sensor_aliases,
}
})
}