// Ce module orchestre le cycle de vie de l'application. use anyhow::Result; use std::time::Instant; use std::collections::HashMap; use std::process::Command; use tokio::time::{interval, sleep, Duration}; use tracing::warn; use crate::config::Config; use crate::commands::{self, CommandAction, CommandValue}; use crate::ha; use crate::mqtt::{self, Backends, Capabilities, Status}; use crate::platform; use crate::telemetry::{BasicTelemetry, TelemetryProvider}; pub struct Runtime { config: Config, start: Instant, } impl Runtime { // Cree un runtime avec la configuration chargee. pub fn new(config: Config) -> Self { Self { config, start: Instant::now(), } } // Demarre la connexion MQTT et boucle sur l'eventloop. pub async fn run(self) -> Result<()> { let handle = mqtt::connect(&self.config)?; let mut event_loop = handle.event_loop; let client = handle.client; // Wait for MQTT connection to be established loop { match event_loop.poll().await { Ok(rumqttc::Event::Incoming(rumqttc::Packet::ConnAck(_))) => { tracing::info!("mqtt connected"); break; } Ok(_) => continue, Err(err) => { tracing::warn!(error = %err, "mqtt connection error, retrying..."); sleep(Duration::from_secs(2)).await; } } } // Spawn event loop handler in background to process messages let (cmd_tx, mut cmd_rx) = tokio::sync::mpsc::unbounded_channel(); tokio::spawn(async move { loop { match event_loop.poll().await { Ok(rumqttc::Event::Incoming(rumqttc::Packet::Publish(publish))) => { let _ = cmd_tx.send((publish.topic.to_string(), publish.payload.to_vec())); } Ok(_) => {} Err(err) => { tracing::warn!(error = %err, "mqtt eventloop error"); tokio::time::sleep(Duration::from_secs(2)).await; } } } }); // Send initial messages if self.config.publish.availability { mqtt::publish_availability(&client, &self.config, true).await?; } let status = build_status(&self.config, self.start.elapsed().as_secs()); mqtt::publish_status(&client, &self.config, &status).await?; mqtt::publish_capabilities(&client, &self.config, &capabilities(&self.config)).await?; if let Err(err) = ha::publish_all(&client, &self.config).await { warn!(error = %err, "ha discovery publish failed"); } publish_initial_command_states(&client, &self.config).await; let initial_power_state = detect_power_state(); if let Err(err) = mqtt::publish_state(&client, &self.config, "power_state", &initial_power_state).await { warn!(error = %err, "publish power_state failed"); } if self.config.features.commands.enabled { mqtt::subscribe_commands(&client, &self.config).await?; } tracing::info!("entering main event loop"); let mut telemetry = if self.config.features.telemetry.enabled { Some(BasicTelemetry::new()) } else { None }; let mut telemetry_tick = interval(Duration::from_secs( self.config.features.telemetry.interval_s, )); let mut heartbeat_tick = interval(Duration::from_secs( self.config.publish.heartbeat_s, )); let mut last_exec: HashMap = HashMap::new(); let shutdown = tokio::signal::ctrl_c(); tokio::pin!(shutdown); loop { tokio::select! { _ = telemetry_tick.tick(), if telemetry.is_some() => { let metrics = telemetry.as_mut().unwrap().read(); for (name, value) in metrics { if let Err(err) = mqtt::publish_state(&client, &self.config, &name, &value).await { warn!(error = %err, "publish state failed"); } } } _ = heartbeat_tick.tick() => { let current = detect_power_state(); if let Err(err) = mqtt::publish_state(&client, &self.config, "power_state", ¤t).await { warn!(error = %err, "publish power_state failed"); } let status = build_status(&self.config, self.start.elapsed().as_secs()); if let Err(err) = mqtt::publish_status(&client, &self.config, &status).await { warn!(error = %err, "publish status failed"); } } Some((topic, payload)) = cmd_rx.recv() => { if let Err(err) = handle_command( &client, &self.config, &mut last_exec, &topic, &payload, ).await { warn!(error = %err, "command handling failed"); } } _ = &mut shutdown => { if let Err(err) = mqtt::publish_availability(&client, &self.config, false).await { warn!(error = %err, "publish availability offline failed"); } if let Err(err) = client.disconnect().await { warn!(error = %err, "mqtt disconnect failed"); } break Ok(()); } } } } } // Genere les capacites declarees par le programme. fn capabilities(cfg: &Config) -> Capabilities { let mut telemetry = Vec::new(); if cfg.features.telemetry.enabled { telemetry.push("cpu_usage".to_string()); telemetry.push("cpu_temp".to_string()); telemetry.push("memory".to_string()); } let mut commands = Vec::new(); if cfg.features.commands.enabled { commands.push("shutdown".to_string()); commands.push("reboot".to_string()); commands.push("sleep".to_string()); commands.push("screen".to_string()); } Capabilities { telemetry, commands, gpu: false, } } // Retourne le backend power actif selon l'OS. fn backend_power(cfg: &Config) -> String { if cfg!(target_os = "windows") { cfg.power_backend.windows.clone() } else { cfg.power_backend.linux.clone() } } // Retourne le backend screen actif selon l'OS. fn backend_screen(cfg: &Config) -> String { if cfg!(target_os = "windows") { cfg.screen_backend.windows.clone() } else { cfg.screen_backend.linux.clone() } } // Construit un status stable (version, OS, uptime, backends). fn build_status(cfg: &Config, uptime_s: u64) -> Status { Status { version: "2.0.0".to_string(), os: std::env::consts::OS.to_string(), uptime_s, last_error: String::new(), backends: Backends { power: backend_power(cfg), screen: backend_screen(cfg), }, } } // Essaie de determiner l'etat d'alimentation sur Linux via systemctl. fn detect_power_state() -> String { if cfg!(target_os = "windows") { return "on".to_string(); } if let Some(state) = detect_power_state_logind() { return state; } match Command::new("systemctl").arg("is-system-running").output() { Ok(output) => { let raw = String::from_utf8_lossy(&output.stdout).trim().to_string(); match raw.as_str() { "running" | "degraded" => "on".to_string(), "stopping" | "starting" => "unknown".to_string(), _ => "unknown".to_string(), } } Err(_) => "unknown".to_string(), } } // Essaie de lire l'etat logind (Active + IdleHint). fn detect_power_state_logind() -> Option { if let Ok(connection) = zbus::blocking::Connection::system() { if let Ok(proxy) = zbus::blocking::Proxy::new( &connection, "org.freedesktop.login1", "/org/freedesktop/login1", "org.freedesktop.login1.Manager", ) { let active: Result = proxy.get_property("IdleHint").map(|v| v); if let Ok(idle_hint) = active { if idle_hint { return Some("idle".to_string()); } return Some("on".to_string()); } } } None } // Traite une commande entrante (topic + payload) avec cooldown et dry-run. async fn handle_command( client: &rumqttc::AsyncClient, cfg: &Config, last_exec: &mut HashMap, topic: &str, payload: &[u8], ) -> anyhow::Result<()> { let action = commands::parse_action(topic)?; let value = commands::parse_value(payload)?; if !commands::allowlist_allows(&cfg.features.commands.allowlist, action) { return Ok(()); } if !commands::allow_command(last_exec, cfg.features.commands.cooldown_s, action) { return Ok(()); } if cfg.features.commands.dry_run { commands::execute_dry_run(action, value)?; publish_command_state(client, cfg, action, value).await?; return Ok(()); } match action { CommandAction::Shutdown => { if matches!(value, CommandValue::Off) { platform::execute_power(&backend_power(cfg), action)?; mqtt::publish_state(client, cfg, "power_state", "off").await?; publish_command_state(client, cfg, action, value).await?; } } CommandAction::Reboot => { if matches!(value, CommandValue::Off) { platform::execute_power(&backend_power(cfg), action)?; mqtt::publish_state(client, cfg, "power_state", "on").await?; publish_command_state(client, cfg, action, value).await?; } } CommandAction::Sleep => { if matches!(value, CommandValue::Off) { platform::execute_power(&backend_power(cfg), action)?; mqtt::publish_state(client, cfg, "power_state", "sleep").await?; publish_command_state(client, cfg, action, value).await?; } } CommandAction::Screen => { platform::execute_screen(&backend_screen(cfg), value)?; publish_command_state(client, cfg, action, value).await?; } } Ok(()) } // Publie l'etat initial des switches HA (par defaut ON). async fn publish_initial_command_states(client: &rumqttc::AsyncClient, cfg: &Config) { let _ = mqtt::publish_state(client, cfg, "shutdown", "ON").await; let _ = mqtt::publish_state(client, cfg, "reboot", "ON").await; let _ = mqtt::publish_state(client, cfg, "sleep", "ON").await; let _ = mqtt::publish_state(client, cfg, "screen", "ON").await; } // Publie l'etat d'une commande pour Home Assistant. async fn publish_command_state( client: &rumqttc::AsyncClient, cfg: &Config, action: CommandAction, value: CommandValue, ) -> anyhow::Result<()> { let state = match value { CommandValue::On => "ON", CommandValue::Off => "OFF", }; let name = commands::action_name(action); mqtt::publish_state(client, cfg, name, state).await }