80 lines
3.3 KiB
Rust
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; });
|
|
}
|
|
}
|
|
}
|