use anyhow::Result; use serde::{Deserialize, Serialize}; use tokio::process::Command; use validator::Validate; use crate::config::validate_unique_id; use super::ModuleContext; const MODULE_ID: &str = "power"; const BUTTON_TRIGGER_TEXT: &str = "press"; #[derive(Serialize, Deserialize, Validate, Clone)] pub(crate) struct ButtonConfig { #[validate(custom = "validate_unique_id")] pub id: String, #[validate(length(min = 1))] pub name: String, #[validate(length(min = 1))] pub command: String, #[serde(default)] pub run_in_shell: bool, } #[derive(Serialize, Deserialize, Validate)] pub(crate) struct Config { #[serde(default)] pub enabled: bool, #[serde(default)] pub buttons: Vec, } pub(crate) async fn init(context: &mut ModuleContext<'_>) -> Result<()> { let config = match &context.config.command_buttons { Some(c) if c.enabled => c, _ => return Ok(()) }; log::info!("Initializing…"); for button in config.buttons.iter() { init_command_button(context, button.clone()).await?; } Ok(()) } async fn init_command_button(context: &mut ModuleContext<'_>, config: ButtonConfig) -> Result<()> { let entity_id = context.get_entity_id(MODULE_ID, &config.id); let command_topic = context.mqtt.get_topic("button", &entity_id, "trigger"); context .mqtt .send_retained_message( context.mqtt.get_topic("button", &entity_id, "config"), json::stringify(json::object! { "availability_topic": context.mqtt.availability_topic, "command_topic": command_topic.as_str(), "device": context.mqtt.discovery_device_object.clone(), "icon": "mdi:power", "name": config.name, "payload_press": BUTTON_TRIGGER_TEXT, "object_id": entity_id.as_str(), "unique_id": entity_id.as_str() }), ) .await?; context.mqtt.subscribe(command_topic, move |text| { if text == BUTTON_TRIGGER_TEXT { run_command(config.command.clone(), config.run_in_shell); } else { log::warn!("Received invalid trigger text for button {}", config.id) } Ok(()) }); Ok(()) } fn run_command(command: String, in_shell: bool) { tokio::spawn(async move { let is_dry_run = cfg!(feature = "dry_run"); let status = { log::info!("Executing command{}: {}", if is_dry_run { " (dry run)" } else { "" }, command); let mut command_parts = command.split(' ').collect::>(); let mut actual_command = if in_shell { let mut c = Command::new("/bin/sh"); c.arg("-lc"); c } else { let c = Command::new(command_parts[0]); command_parts.remove(0); c }; actual_command.args(command_parts); if is_dry_run { None } else { Some(actual_command.status()) } }; if let Some(f) = status { f.await?; } anyhow::Ok(()) }); }