Files
gree-controller/src/state.rs
T
2026-08-27 22:00:13 +02:00

80 lines
3.3 KiB
Rust

use std::{collections::HashMap, sync::{Arc, atomic::AtomicBool}, time::Instant};
use chrono::Utc;
use serde_json::Value;
use tokio::sync::{broadcast, Mutex, Notify, OwnedMutexGuard, RwLock};
use crate::{config::Config, db::Db, models::{ApiEvent, DeviceCommand, RuntimeSettings}, protocol::GreeClient};
#[derive(Debug, Clone)]
pub(crate) struct PendingControllerCommand {
/// Recent controller-requested values for climate fields. Keep a short history because
/// some GREE modules expose intermediate/out-of-order status snapshots while settling.
pub commands: Vec<DeviceCommand>,
/// Matching pre-command values for the same fields. A brief rollback to one of these
/// values is firmware settling, not necessarily a physical-remote takeover.
pub baselines: Vec<DeviceCommand>,
pub expires_at: Instant,
}
#[derive(Clone)]
pub struct AppState {
pub db: Db,
pub settings: Arc<RwLock<RuntimeSettings>>,
pub config: Arc<Config>,
pub gree: GreeClient,
pub events: broadcast::Sender<ApiEvent>,
pub http: reqwest::Client,
pub outdoor_temperature: Arc<RwLock<Option<f64>>>,
pub debug_gree_frames: Arc<AtomicBool>,
/// Thermostat/automation control stays passive until every enabled device has had
/// one startup poll, preventing stale persisted device state from causing restart commands.
pub initial_device_sync_complete: Arc<AtomicBool>,
/// Explicit thermostat changes wake the regulator instead of waiting for the next fixed interval.
pub zone_control_wakeup: Arc<Notify>,
pub(crate) device_operation_locks: Arc<Mutex<HashMap<String, Arc<Mutex<()>>>>>,
/// Short-lived expected climate state from controller-originated commands. It prevents
/// a delayed GREE status update from being mistaken for remote/manual takeover.
pub(crate) pending_controller_commands: Arc<Mutex<HashMap<String, PendingControllerCommand>>>,
pub started: Instant,
}
impl AppState {
pub async fn lock_device_operation(&self, device_id: &str) -> OwnedMutexGuard<()> {
let lock = {
let mut locks = self.device_operation_locks.lock().await;
locks.entry(device_id.to_string()).or_insert_with(|| Arc::new(Mutex::new(()))).clone()
};
lock.lock_owned().await
}
pub fn wake_zone_control(&self) {
self.zone_control_wakeup.notify_one();
}
pub fn broadcast(&self, event: impl Into<String>, data: Value) {
let _ = self.events.send(ApiEvent {
event: event.into(),
timestamp: Utc::now(),
data,
});
}
pub fn log(&self, level: &str, kind: &str, message: &str, metadata: Value) {
if let Err(err) = self.db.log_event(level, kind, message, &metadata) {
tracing::warn!(error=?err, "cannot persist event log");
}
self.broadcast("log.created", serde_json::json!({
"level": level,
"kind": kind,
"message": message,
"metadata": metadata,
}));
if let Ok(handle) = tokio::runtime::Handle::try_current() {
let state = self.clone();
let level = level.to_string();
let kind = kind.to_string();
let message = message.to_string();
handle.spawn(async move { crate::notifications::dispatch(state, level, kind, message, metadata).await; });
}
}
}