Skip to main content

govee/
capability.rs

1use serde::{Deserialize, Serialize};
2
3/// Top-level capability entry returned by the Govee v2 API.
4#[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/// The typed parameters for a capability, tagged by `dataType`.
13#[derive(Debug, Clone, Serialize)] // custom Deserialize below
14pub 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    /// Forward-compatibility catch-all for unknown `dataType` values.
25    /// Preserves the raw JSON payload for forward compatibility.
26    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/// A single option in an ENUM capability.
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct EnumOption {
56    pub name: String,
57    pub value: serde_json::Value,
58}
59
60/// The range descriptor for an INTEGER capability.
61#[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/// A single field in a STRUCT capability.
70#[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/// The current state of a single capability instance.
78#[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/// The value wrapper inside a `CapabilityState`.
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct StateValue {
89    pub value: serde_json::Value,
90}
91
92/// Control value for issuing commands to a device.
93#[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/// A dynamic scene identifier, which may be either a preset (with a `paramId`) or a DIY index.
117///
118/// Uses `#[serde(untagged)]` — `Preset` is tried first (order matters).
119#[derive(Debug, Clone, Serialize, Deserialize)]
120#[serde(untagged)]
121pub enum DynamicSceneValue {
122    /// A preset scene identified by `paramId` and `id`.
123    Preset {
124        #[serde(rename = "paramId")]
125        param_id: u32,
126        id: u32,
127    },
128    /// A DIY scene identified by a plain integer index.
129    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        // DynamicSceneValue is untagged so Preset serializes as object
265        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}