resymo_agent/command/
exec.rs

1use crate::{
2    command::CallbackFn,
3    config::CommonCommand,
4    uplink::homeassistant::{PAYLOAD_RUNNING, PAYLOAD_STOPPED},
5    utils::is_default,
6};
7use async_trait::async_trait;
8use homeassistant_agent::model::{Availability, Discovery};
9use std::{borrow::Cow, collections::HashMap, ops::Deref};
10
11#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
12#[serde(rename_all = "camelCase")]
13pub struct Configuration {
14    #[serde(flatten)]
15    pub common: CommonCommand,
16
17    /// execution tasks
18    #[serde(default)]
19    pub items: HashMap<String, Run>,
20}
21
22impl Deref for Configuration {
23    type Target = CommonCommand;
24
25    fn deref(&self) -> &Self::Target {
26        &self.common
27    }
28}
29
30#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
31#[serde(rename_all = "camelCase")]
32pub struct Run {
33    /// The binary to call
34    pub command: String,
35
36    /// The arguments
37    #[serde(default, skip_serializing_if = "Vec::is_empty")]
38    pub args: Vec<String>,
39
40    /// The environment variables
41    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
42    pub envs: HashMap<String, String>,
43
44    #[serde(default, skip_serializing_if = "is_default")]
45    pub clean_env: bool,
46
47    /// The Home Assistant discovery section
48    #[serde(default, skip_serializing_if = "Option::is_none")]
49    pub discovery: Option<Discovery>,
50}
51
52pub struct Command {
53    config: Run,
54    discovery: Option<Discovery>,
55}
56
57impl Command {
58    pub fn new(config: Configuration) -> HashMap<String, Command> {
59        config
60            .items
61            .into_iter()
62            .map(|(name, config)| {
63                let command = Self::new_run(&name, config);
64                (name, command)
65            })
66            .collect()
67    }
68
69    fn new_run(name: &str, config: Run) -> Self {
70        let discovery = if let Some(mut discovery) = config.discovery.clone() {
71            if discovery.unique_id.is_none() {
72                discovery.unique_id = Some(name.into());
73            }
74
75            discovery.availability = vec![Availability::new("state")
76                .payload_available(PAYLOAD_STOPPED)
77                .payload_not_available(PAYLOAD_RUNNING)];
78
79            Some(discovery)
80        } else {
81            None
82        };
83
84        Self { config, discovery }
85    }
86}
87
88#[async_trait(?Send)]
89impl super::Command for Command {
90    async fn start(&self, payload: Cow<'_, str>, callback: Box<CallbackFn>) {
91        log::info!("running command: {payload}");
92
93        let mut cmd = tokio::process::Command::new(&self.config.command);
94
95        if self.config.clean_env {
96            cmd.env_clear();
97        }
98
99        cmd.args(self.config.args.clone())
100            .envs(self.config.envs.clone());
101
102        tokio::spawn(async move {
103            let result = match cmd.output().await {
104                Ok(output) if output.status.success() => Ok(()),
105                Ok(_) => Err(()),
106                Err(err) => {
107                    log::warn!("Failed to launch command: {err}");
108                    Err(())
109                }
110            };
111
112            (callback)(result).await;
113        });
114    }
115
116    fn describe_ha(&self) -> Option<Discovery> {
117        self.discovery.clone()
118    }
119}