Files
gree-controller/src/main.rs
T

149 lines
5.0 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 anyhow::{Context, Result};
use config::Config;
use db::Db;
use models::Device;
use protocol::GreeClient;
use state::AppState;
use std::{
sync::{atomic::AtomicBool, Arc},
time::{Duration, Instant},
};
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 => {} }
}