1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize)]
5pub struct Capability {
6 #[serde(rename = "type")]
7 pub type_: String,
8 pub instance: String,
9 pub parameters: CapabilityParameters,
10}
11
12#[derive(Debug, Clone, Serialize)] pub enum CapabilityParameters {
15 #[serde(rename = "ENUM")]
16 Enum { options: Vec<EnumOption> },
17 #[serde(rename = "INTEGER")]
18 Integer {
19 #[serde(flatten)]
20 range: IntRange,
21 },
22 #[serde(rename = "STRUCT")]
23 Struct { fields: Vec<StructField> },
24 Unknown(serde_json::Value),
27}
28
29impl<'de> Deserialize<'de> for CapabilityParameters {
30 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
31 let value = serde_json::Value::deserialize(d)?;
32 match value.get("dataType").and_then(|v| v.as_str()) {
33 Some("ENUM") => {
34 let options = serde_json::from_value(value["options"].clone())
35 .map_err(serde::de::Error::custom)?;
36 Ok(CapabilityParameters::Enum { options })
37 }
38 Some("INTEGER") => {
39 let range =
40 serde_json::from_value(value.clone()).map_err(serde::de::Error::custom)?;
41 Ok(CapabilityParameters::Integer { range })
42 }
43 Some("STRUCT") => {
44 let fields = serde_json::from_value(value["fields"].clone())
45 .map_err(serde::de::Error::custom)?;
46 Ok(CapabilityParameters::Struct { fields })
47 }
48 _ => Ok(CapabilityParameters::Unknown(value)),
49 }
50 }
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct EnumOption {
56 pub name: String,
57 pub value: serde_json::Value,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct IntRange {
63 pub min: i64,
64 pub max: i64,
65 pub precision: i64,
66 pub unit: Option<String>,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71#[serde(rename_all = "camelCase")]
72pub struct StructField {
73 pub field_name: String,
74 pub data_type: String,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct CapabilityState {
80 #[serde(rename = "type")]
81 pub type_: String,
82 pub instance: String,
83 pub state: StateValue,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct StateValue {
89 pub value: serde_json::Value,
90}
91
92#[derive(Debug, Clone, Serialize)]
94pub enum CapabilityValue {
95 OnOff(u8),
96 Rgb(u32),
97 ColorTempK(u32),
98 Brightness(u8),
99 WorkMode {
100 work_mode: u32,
101 mode_value: Option<u32>,
102 },
103 DynamicScene(DynamicSceneValue),
104 DiyScene(u32),
105 SegmentColor {
106 segments: Vec<u8>,
107 rgb: u32,
108 },
109 SegmentBrightness {
110 segments: Vec<u8>,
111 brightness: u8,
112 },
113 Raw(serde_json::Value),
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
120#[serde(untagged)]
121pub enum DynamicSceneValue {
122 Preset {
124 #[serde(rename = "paramId")]
125 param_id: u32,
126 id: u32,
127 },
128 Diy(u32),
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn capability_parameters_enum_variant() {
138 let json =
139 r#"{"dataType":"ENUM","options":[{"name":"on","value":1},{"name":"off","value":0}]}"#;
140 let p: CapabilityParameters = serde_json::from_str(json).unwrap();
141 match p {
142 CapabilityParameters::Enum { options } => {
143 assert_eq!(options.len(), 2);
144 assert_eq!(options[0].name, "on");
145 assert_eq!(options[0].value, serde_json::json!(1));
146 }
147 other => panic!("expected Enum, got {other:?}"),
148 }
149 }
150
151 #[test]
152 fn capability_parameters_integer_variant() {
153 let json = r#"{"dataType":"INTEGER","min":0,"max":100,"precision":1,"unit":"percent"}"#;
154 let p: CapabilityParameters = serde_json::from_str(json).unwrap();
155 match p {
156 CapabilityParameters::Integer { range } => {
157 assert_eq!(range.min, 0);
158 assert_eq!(range.max, 100);
159 assert_eq!(range.precision, 1);
160 assert_eq!(range.unit.as_deref(), Some("percent"));
161 }
162 other => panic!("expected Integer, got {other:?}"),
163 }
164 }
165
166 #[test]
167 fn capability_parameters_struct_variant() {
168 let json = r#"{"dataType":"STRUCT","fields":[{"fieldName":"colorTemInKelvin","dataType":"INTEGER"}]}"#;
169 let p: CapabilityParameters = serde_json::from_str(json).unwrap();
170 match p {
171 CapabilityParameters::Struct { fields } => {
172 assert_eq!(fields.len(), 1);
173 assert_eq!(fields[0].field_name, "colorTemInKelvin");
174 assert_eq!(fields[0].data_type, "INTEGER");
175 }
176 other => panic!("expected Struct, got {other:?}"),
177 }
178 }
179
180 #[test]
181 fn capability_parameters_unknown_variant_preserves_payload() {
182 let json = r#"{"dataType":"FUTURE_TYPE","someField":42}"#;
183 let p: CapabilityParameters = serde_json::from_str(json).unwrap();
184 match p {
185 CapabilityParameters::Unknown(v) => {
186 assert_eq!(v["dataType"], "FUTURE_TYPE");
187 assert_eq!(v["someField"], 42);
188 }
189 other => panic!("expected Unknown, got {other:?}"),
190 }
191 }
192
193 #[test]
194 fn dynamic_scene_value_preset() {
195 let json = r#"{"paramId":1,"id":2}"#;
196 let v: DynamicSceneValue = serde_json::from_str(json).unwrap();
197 match v {
198 DynamicSceneValue::Preset { param_id, id } => {
199 assert_eq!(param_id, 1);
200 assert_eq!(id, 2);
201 }
202 other => panic!("expected Preset, got {other:?}"),
203 }
204 }
205
206 #[test]
207 fn dynamic_scene_value_diy() {
208 let json = "42";
209 let v: DynamicSceneValue = serde_json::from_str(json).unwrap();
210 match v {
211 DynamicSceneValue::Diy(n) => assert_eq!(n, 42),
212 other => panic!("expected Diy, got {other:?}"),
213 }
214 }
215
216 #[test]
217 fn capability_state_round_trip() {
218 let original = CapabilityState {
219 type_: "devices.capabilities.on_off".to_string(),
220 instance: "powerSwitch".to_string(),
221 state: StateValue {
222 value: serde_json::json!(1),
223 },
224 };
225 let json = serde_json::to_string(&original).unwrap();
226 let deserialized: CapabilityState = serde_json::from_str(&json).unwrap();
227 assert_eq!(deserialized.type_, original.type_);
228 assert_eq!(deserialized.instance, original.instance);
229 assert_eq!(deserialized.state.value, original.state.value);
230 }
231
232 #[test]
233 fn capability_value_on_off_serializes() {
234 let v = CapabilityValue::OnOff(1);
235 let json = serde_json::to_value(&v).unwrap();
236 assert_eq!(json, serde_json::json!({ "OnOff": 1 }));
237 }
238
239 #[test]
240 fn capability_value_rgb_serializes() {
241 let v = CapabilityValue::Rgb(0xFF0000);
242 let json = serde_json::to_value(&v).unwrap();
243 assert_eq!(json, serde_json::json!({ "Rgb": 0xFF0000u32 }));
244 }
245
246 #[test]
247 fn capability_value_color_temp_k_serializes() {
248 let v = CapabilityValue::ColorTempK(6500);
249 let json = serde_json::to_value(&v).unwrap();
250 assert_eq!(json, serde_json::json!({ "ColorTempK": 6500 }));
251 }
252
253 #[test]
254 fn capability_value_brightness_serializes() {
255 let v = CapabilityValue::Brightness(80);
256 let json = serde_json::to_value(&v).unwrap();
257 assert_eq!(json, serde_json::json!({ "Brightness": 80 }));
258 }
259
260 #[test]
261 fn capability_value_dynamic_scene_preset_round_trips() {
262 let v = CapabilityValue::DynamicScene(DynamicSceneValue::Preset { param_id: 1, id: 2 });
263 let json = serde_json::to_value(&v).unwrap();
264 assert_eq!(
266 json,
267 serde_json::json!({ "DynamicScene": { "paramId": 1, "id": 2 } })
268 );
269 }
270
271 #[test]
272 fn capability_value_diy_scene_serializes() {
273 let v = CapabilityValue::DiyScene(42);
274 let json = serde_json::to_value(&v).unwrap();
275 assert_eq!(json, serde_json::json!({ "DiyScene": 42 }));
276 }
277}