resymo_agent/command/
exec.rs1use 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 #[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 pub command: String,
35
36 #[serde(default, skip_serializing_if = "Vec::is_empty")]
38 pub args: Vec<String>,
39
40 #[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 #[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}