use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Capability {
#[serde(rename = "type")]
pub type_: String,
pub instance: String,
pub parameters: CapabilityParameters,
}
#[derive(Debug, Clone, Serialize)] pub enum CapabilityParameters {
#[serde(rename = "ENUM")]
Enum { options: Vec<EnumOption> },
#[serde(rename = "INTEGER")]
Integer {
#[serde(flatten)]
range: IntRange,
},
#[serde(rename = "STRUCT")]
Struct { fields: Vec<StructField> },
Unknown(serde_json::Value),
}
impl<'de> Deserialize<'de> for CapabilityParameters {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let value = serde_json::Value::deserialize(d)?;
match value.get("dataType").and_then(|v| v.as_str()) {
Some("ENUM") => {
let options = serde_json::from_value(value["options"].clone())
.map_err(serde::de::Error::custom)?;
Ok(CapabilityParameters::Enum { options })
}
Some("INTEGER") => {
let range =
serde_json::from_value(value.clone()).map_err(serde::de::Error::custom)?;
Ok(CapabilityParameters::Integer { range })
}
Some("STRUCT") => {
let fields = serde_json::from_value(value["fields"].clone())
.map_err(serde::de::Error::custom)?;
Ok(CapabilityParameters::Struct { fields })
}
_ => Ok(CapabilityParameters::Unknown(value)),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnumOption {
pub name: String,
pub value: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IntRange {
pub min: i64,
pub max: i64,
pub precision: i64,
pub unit: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StructField {
pub field_name: String,
pub data_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CapabilityState {
#[serde(rename = "type")]
pub type_: String,
pub instance: String,
pub state: StateValue,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StateValue {
pub value: serde_json::Value,
}
#[derive(Debug, Clone, Serialize)]
pub enum CapabilityValue {
OnOff(u8),
Rgb(u32),
ColorTempK(u32),
Brightness(u8),
WorkMode {
work_mode: u32,
mode_value: Option<u32>,
},
DynamicScene(DynamicSceneValue),
DiyScene(u32),
SegmentColor {
segments: Vec<u8>,
rgb: u32,
},
SegmentBrightness {
segments: Vec<u8>,
brightness: u8,
},
Raw(serde_json::Value),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum DynamicSceneValue {
Preset {
#[serde(rename = "paramId")]
param_id: u32,
id: u32,
},
Diy(u32),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn capability_parameters_enum_variant() {
let json =
r#"{"dataType":"ENUM","options":[{"name":"on","value":1},{"name":"off","value":0}]}"#;
let p: CapabilityParameters = serde_json::from_str(json).unwrap();
match p {
CapabilityParameters::Enum { options } => {
assert_eq!(options.len(), 2);
assert_eq!(options[0].name, "on");
assert_eq!(options[0].value, serde_json::json!(1));
}
other => panic!("expected Enum, got {other:?}"),
}
}
#[test]
fn capability_parameters_integer_variant() {
let json = r#"{"dataType":"INTEGER","min":0,"max":100,"precision":1,"unit":"percent"}"#;
let p: CapabilityParameters = serde_json::from_str(json).unwrap();
match p {
CapabilityParameters::Integer { range } => {
assert_eq!(range.min, 0);
assert_eq!(range.max, 100);
assert_eq!(range.precision, 1);
assert_eq!(range.unit.as_deref(), Some("percent"));
}
other => panic!("expected Integer, got {other:?}"),
}
}
#[test]
fn capability_parameters_struct_variant() {
let json = r#"{"dataType":"STRUCT","fields":[{"fieldName":"colorTemInKelvin","dataType":"INTEGER"}]}"#;
let p: CapabilityParameters = serde_json::from_str(json).unwrap();
match p {
CapabilityParameters::Struct { fields } => {
assert_eq!(fields.len(), 1);
assert_eq!(fields[0].field_name, "colorTemInKelvin");
assert_eq!(fields[0].data_type, "INTEGER");
}
other => panic!("expected Struct, got {other:?}"),
}
}
#[test]
fn capability_parameters_unknown_variant_preserves_payload() {
let json = r#"{"dataType":"FUTURE_TYPE","someField":42}"#;
let p: CapabilityParameters = serde_json::from_str(json).unwrap();
match p {
CapabilityParameters::Unknown(v) => {
assert_eq!(v["dataType"], "FUTURE_TYPE");
assert_eq!(v["someField"], 42);
}
other => panic!("expected Unknown, got {other:?}"),
}
}
#[test]
fn dynamic_scene_value_preset() {
let json = r#"{"paramId":1,"id":2}"#;
let v: DynamicSceneValue = serde_json::from_str(json).unwrap();
match v {
DynamicSceneValue::Preset { param_id, id } => {
assert_eq!(param_id, 1);
assert_eq!(id, 2);
}
other => panic!("expected Preset, got {other:?}"),
}
}
#[test]
fn dynamic_scene_value_diy() {
let json = "42";
let v: DynamicSceneValue = serde_json::from_str(json).unwrap();
match v {
DynamicSceneValue::Diy(n) => assert_eq!(n, 42),
other => panic!("expected Diy, got {other:?}"),
}
}
#[test]
fn capability_state_round_trip() {
let original = CapabilityState {
type_: "devices.capabilities.on_off".to_string(),
instance: "powerSwitch".to_string(),
state: StateValue {
value: serde_json::json!(1),
},
};
let json = serde_json::to_string(&original).unwrap();
let deserialized: CapabilityState = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.type_, original.type_);
assert_eq!(deserialized.instance, original.instance);
assert_eq!(deserialized.state.value, original.state.value);
}
#[test]
fn capability_value_on_off_serializes() {
let v = CapabilityValue::OnOff(1);
let json = serde_json::to_value(&v).unwrap();
assert_eq!(json, serde_json::json!({ "OnOff": 1 }));
}
#[test]
fn capability_value_rgb_serializes() {
let v = CapabilityValue::Rgb(0xFF0000);
let json = serde_json::to_value(&v).unwrap();
assert_eq!(json, serde_json::json!({ "Rgb": 0xFF0000u32 }));
}
#[test]
fn capability_value_color_temp_k_serializes() {
let v = CapabilityValue::ColorTempK(6500);
let json = serde_json::to_value(&v).unwrap();
assert_eq!(json, serde_json::json!({ "ColorTempK": 6500 }));
}
#[test]
fn capability_value_brightness_serializes() {
let v = CapabilityValue::Brightness(80);
let json = serde_json::to_value(&v).unwrap();
assert_eq!(json, serde_json::json!({ "Brightness": 80 }));
}
#[test]
fn capability_value_dynamic_scene_preset_round_trips() {
let v = CapabilityValue::DynamicScene(DynamicSceneValue::Preset { param_id: 1, id: 2 });
let json = serde_json::to_value(&v).unwrap();
assert_eq!(
json,
serde_json::json!({ "DynamicScene": { "paramId": 1, "id": 2 } })
);
}
#[test]
fn capability_value_diy_scene_serializes() {
let v = CapabilityValue::DiyScene(42);
let json = serde_json::to_value(&v).unwrap();
assert_eq!(json, serde_json::json!({ "DiyScene": 42 }));
}
}