Files
gree-controller/src/main.rs
T
2026-09-03 13:08:28 +02:00

127 lines
4.8 KiB
Rust

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 => {} }
}