mod api; mod config; mod db; mod engine; mod error; mod home_assistant; mod influxdb; mod models; mod notifications; mod protocol; mod queries; mod state; use std::{sync::{Arc, atomic::AtomicBool}, time::{Duration, Instant}}; use anyhow::{Context, Result}; use config::Config; use db::Db; use models::Device; use protocol::GreeClient; use state::AppState; use tokio::{net::TcpListener, signal, sync::{broadcast, Notify, RwLock}}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; #[tokio::main] async fn main() -> Result<()> { let config = Config::load()?; init_tracing(); let db = Db::open(&config.database)?; let mut runtime_settings = db.load_runtime_settings()?.unwrap_or_else(|| config.runtime_defaults()); // Network deployment settings explicitly provided by the service environment are authoritative. // This makes /etc/gree-controller.env useful even after runtime settings were persisted in SQLite. if std::env::var_os("GREE_CONTROLLER_DISCOVERY_BROADCAST").is_some() { runtime_settings.discovery_broadcast = config.discovery_broadcast.clone(); } config.apply_runtime_env_overrides(&mut runtime_settings); db.save_runtime_settings(&runtime_settings)?; if config.simulate && config.auto_seed && db.count_devices()? == 0 { db.save_device(&Device::simulated_default())?; db.log_event( "info", "simulator.seeded", "Created the default simulator device", &serde_json::json!({"device_id":"sim-salon"}), )?; } let (events, _) = broadcast::channel(512); let debug_gree_frames = Arc::new(AtomicBool::new(runtime_settings.debug.gree_frames)); let http = reqwest::Client::builder() .timeout(Duration::from_secs(10)) .user_agent(concat!("gree-controller/", env!("CARGO_PKG_VERSION"))) .build()?; let state = AppState { db, settings: Arc::new(RwLock::new(runtime_settings.clone())), config: Arc::new(config.clone()), gree: GreeClient::new( runtime_settings.controller_id.clone(), (!config.gree_interface.trim().is_empty()).then(|| config.gree_interface.trim().to_string()), Some(events.clone()), debug_gree_frames.clone(), ), events, http, outdoor_temperature: Arc::new(RwLock::new(None)), debug_gree_frames, initial_device_sync_complete: Arc::new(AtomicBool::new(false)), zone_control_wakeup: Arc::new(Notify::new()), device_operation_locks: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())), zone_operation_locks: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())), group_operation_locks: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())), schedule_operation_lock: Arc::new(tokio::sync::Mutex::new(())), automation_operation_lock: Arc::new(tokio::sync::Mutex::new(())), house_operation_lock: Arc::new(tokio::sync::Mutex::new(())), configuration_operation_lock: Arc::new(tokio::sync::Mutex::new(())), zone_control_cycle_lock: Arc::new(tokio::sync::Mutex::new(())), pending_controller_commands: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())), started: Instant::now(), }; engine::start(state.clone()); let app = api::router(state.clone()); let listener = TcpListener::bind(config.bind).await .with_context(|| format!("cannot bind HTTP server to {}", config.bind))?; let gree_interface_log = if config.gree_interface.trim().is_empty() { "auto" } else { config.gree_interface.trim() }; tracing::info!( address = %config.bind, database = %config.database.display(), simulator = config.simulate, auth = !config.app_token.trim().is_empty(), gree_interface = %gree_interface_log, discovery_broadcast = %runtime_settings.discovery_broadcast, "GREE Controller started" ); axum::serve(listener, app) .with_graceful_shutdown(shutdown_signal()) .await?; tracing::info!("GREE Controller stopped"); Ok(()) } fn init_tracing() { let filter = tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| "info,tower_http=info".into()); tracing_subscriber::registry() .with(filter) .with(tracing_subscriber::fmt::layer().compact()) .init(); } async fn shutdown_signal() { let ctrl_c = async { signal::ctrl_c().await.expect("cannot install Ctrl+C handler"); }; #[cfg(unix)] let terminate = async { signal::unix::signal(signal::unix::SignalKind::terminate()) .expect("cannot install SIGTERM handler") .recv().await; }; #[cfg(not(unix))] let terminate = std::future::pending::<()>(); tokio::select! { _ = ctrl_c => {}, _ = terminate => {} } }