Skip to main content

homeassistant_agent/model/
discovery.rs

1use crate::{model::Device, utils::is_default};
2
3// also see: https://developers.home-assistant.io/docs/core/entity/
4
5/// Discovery message
6#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
7#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
8pub struct Discovery {
9    /// The name of the application that is the origin the discovered MQTT item. This option is required.
10    // Don't skip serde if it's empty, as it has to be null then
11    #[serde(default)]
12    pub name: Option<String>,
13
14    #[serde(default, skip_serializing_if = "Option::is_none")]
15    pub unique_id: Option<String>,
16
17    #[serde(default, skip_serializing_if = "Option::is_none")]
18    pub device: Option<Device>,
19
20    /// The device class. Should be `null` if omitted, so don't skip.
21    #[serde(default)]
22    pub device_class: Option<String>,
23
24    #[serde(default, skip_serializing_if = "Option::is_none")]
25    pub state_class: Option<StateClass>,
26
27    #[serde(default, skip_serializing_if = "Option::is_none")]
28    pub command_topic: Option<String>,
29
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub command_template: Option<String>,
32
33    #[serde(default, skip_serializing_if = "Option::is_none")]
34    pub state_topic: Option<String>,
35
36    #[serde(default, skip_serializing_if = "Option::is_none")]
37    pub unit_of_measurement: Option<String>,
38
39    #[serde(default, skip_serializing_if = "Option::is_none")]
40    pub value_template: Option<String>,
41
42    #[serde(default, skip_serializing_if = "Option::is_none")]
43    pub enabled_by_default: Option<bool>,
44
45    #[serde(default, skip_serializing_if = "is_default")]
46    pub availability_mode: AvailabilityMode,
47
48    #[serde(default, skip_serializing_if = "Vec::is_empty")]
49    pub availability: Vec<Availability>,
50}
51
52#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
53#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
54pub struct Availability {
55    pub topic: String,
56
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub payload_available: Option<String>,
59
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub payload_not_available: Option<String>,
62
63    #[serde(default, skip_serializing_if = "Option::is_none")]
64    pub value_template: Option<String>,
65}
66
67impl Availability {
68    pub fn new(topic: impl Into<String>) -> Self {
69        Self {
70            topic: topic.into(),
71            payload_available: None,
72            payload_not_available: None,
73            value_template: None,
74        }
75    }
76
77    pub fn payload_available(mut self, payload: impl Into<String>) -> Self {
78        self.payload_available = Some(payload.into());
79        self
80    }
81
82    pub fn payload_not_available(mut self, payload: impl Into<String>) -> Self {
83        self.payload_not_available = Some(payload.into());
84        self
85    }
86
87    pub fn value_template(mut self, value_template: impl Into<String>) -> Self {
88        self.value_template = Some(value_template.into());
89        self
90    }
91}
92
93#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
94#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
95#[serde(rename_all = "snake_case")]
96pub enum AvailabilityMode {
97    All,
98    Any,
99    #[default]
100    Latest,
101}
102
103#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
104#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
105#[serde(rename_all = "snake_case")]
106pub enum StateClass {
107    Measurement,
108    Total,
109    TotalIncreasing,
110}
111
112#[cfg(test)]
113mod test {
114    use super::*;
115    use serde_json::json;
116
117    #[test]
118    fn test_serde() {
119        assert_eq!(
120            serde_json::to_value(Discovery {
121                device: Some(Device {
122                    identifiers: vec!["test-id1".into()],
123                    name: Some("Test Device 1".to_string()),
124                    base_topic: None,
125                    sw_version: None,
126                    support_url: None,
127                }),
128                device_class: Some("motion".to_string()),
129                state_topic: Some("some/topic".to_string()),
130                ..Default::default()
131            })
132            .unwrap(),
133            json!({
134                "name": null,
135                "device_class": "motion",
136                "device": {
137                    "identifiers" : [
138                        "test-id1"
139                    ],
140                    "name": "Test Device 1",
141                },
142                "state_topic": "some/topic"
143            })
144        )
145    }
146}