use super::*; use crate::atx::{AtxDriverType, AtxState, HddStatus, PowerStatus}; const WOL_HISTORY_DEFAULT_LIMIT: usize = 5; const WOL_HISTORY_MAX_LIMIT: usize = 50; /// ATX state response #[derive(Serialize)] pub struct AtxStateResponse { pub available: bool, pub backend: String, pub initialized: bool, pub power_status: String, pub led_supported: bool, pub hdd_status: String, pub hdd_supported: bool, } impl From for AtxStateResponse { fn from(state: AtxState) -> Self { Self { available: state.available, backend: match state.driver { AtxDriverType::Gpio => "gpio", AtxDriverType::UsbRelay => "usbrelay", AtxDriverType::Serial => "serial", AtxDriverType::None => "none", } .to_string(), initialized: state.power_configured || state.reset_configured, power_status: match state.power_status { PowerStatus::On => "on".to_string(), PowerStatus::Off => "off".to_string(), PowerStatus::Unknown => "unknown".to_string(), }, led_supported: state.led_supported, hdd_status: match state.hdd_status { HddStatus::Active => "active".to_string(), HddStatus::Inactive => "inactive".to_string(), HddStatus::Unknown => "unknown".to_string(), }, hdd_supported: state.hdd_supported, } } } /// Get ATX status pub async fn atx_status(State(state): State>) -> Result> { let atx_guard = state.atx.read().await; match atx_guard.as_ref() { Some(atx) => { let atx_state = atx.state().await; Ok(Json(AtxStateResponse::from(atx_state))) } None => Ok(Json(AtxStateResponse { available: false, backend: "none".to_string(), initialized: false, power_status: "unknown".to_string(), led_supported: false, hdd_status: "unknown".to_string(), hdd_supported: false, })), } } /// ATX power control request #[derive(Deserialize)] pub struct AtxPowerControlRequest { pub action: String, // "short", "long", "reset" } /// Control ATX power pub async fn atx_power( State(state): State>, Json(req): Json, ) -> Result> { let atx_guard = state.atx.read().await; let atx = atx_guard .as_ref() .ok_or_else(|| AppError::Internal("ATX controller not initialized".to_string()))?; match req.action.as_str() { "short" => { atx.power_short().await?; Ok(Json(LoginResponse { success: true, message: Some("Power short press executed".to_string()), })) } "long" => { atx.power_long().await?; Ok(Json(LoginResponse { success: true, message: Some("Power long press (force off) executed".to_string()), })) } "reset" => { atx.reset().await?; Ok(Json(LoginResponse { success: true, message: Some("Reset button pressed".to_string()), })) } _ => Err(AppError::BadRequest(format!( "Unknown ATX action: {}. Valid actions: short, long, reset", req.action ))), } } /// WOL request body #[derive(Debug, Deserialize)] pub struct WolRequest { /// Target MAC address (e.g., "AA:BB:CC:DD:EE:FF" or "AA-BB-CC-DD-EE-FF") pub mac_address: String, } #[derive(Debug, Deserialize, Default)] pub struct WolHistoryQuery { /// Maximum history entries to return pub limit: Option, } #[derive(Debug, Serialize)] pub struct WolHistoryEntry { pub mac_address: String, pub updated_at: i64, } #[derive(Debug, Serialize)] pub struct WolHistoryResponse { pub history: Vec, } fn normalize_wol_mac_address(mac_address: &str) -> String { let normalized = mac_address.trim().to_uppercase().replace('-', ":"); if normalized.len() == 12 && normalized.chars().all(|c| c.is_ascii_hexdigit()) { let mut mac_with_separator = String::with_capacity(17); for (index, chunk) in normalized.as_bytes().chunks(2).enumerate() { if index > 0 { mac_with_separator.push(':'); } mac_with_separator.push(chunk[0] as char); mac_with_separator.push(chunk[1] as char); } mac_with_separator } else { normalized } } /// Send Wake-on-LAN magic packet pub async fn atx_wol( State(state): State>, Json(req): Json, ) -> Result> { let mac_address = normalize_wol_mac_address(&req.mac_address); // Get WOL interface from config let config = state.config.get(); let interface = if config.atx.wol_interface.is_empty() { None } else { Some(config.atx.wol_interface.as_str()) }; // Send WOL packet crate::atx::send_wol(&mac_address, interface)?; if let Err(error) = crate::atx::record_wol_history(state.db.pool(), &mac_address).await { warn!("Failed to persist WOL history: {}", error); } Ok(Json(LoginResponse { success: true, message: Some(format!("WOL packet sent to {}", mac_address)), })) } /// Get WOL history pub async fn atx_wol_history( State(state): State>, Query(query): Query, ) -> Result> { let limit = query .limit .unwrap_or(WOL_HISTORY_DEFAULT_LIMIT) .clamp(1, WOL_HISTORY_MAX_LIMIT); let rows = crate::atx::list_wol_history(state.db.pool(), limit).await?; let history = rows .into_iter() .map(|(mac_address, updated_at)| WolHistoryEntry { mac_address, updated_at, }) .collect(); Ok(Json(WolHistoryResponse { history })) }